@asteroidai/cli 1.9.12 → 1.9.27

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/dist/asteroid.js CHANGED
@@ -4616,8 +4616,8 @@ var HIDDEN = /^agent[A-Z]|Url$/;
4616
4616
  var MAX_COLUMNS = 6;
4617
4617
  var MAX_CELL = 60;
4618
4618
  var CONTROL = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[@-_]?|[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
4619
- function clean(text2) {
4620
- return text2.replace(CONTROL, "");
4619
+ function clean(text3) {
4620
+ return text3.replace(CONTROL, "");
4621
4621
  }
4622
4622
  function isObject(value2) {
4623
4623
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
@@ -4625,8 +4625,8 @@ function isObject(value2) {
4625
4625
  function isScalar(value2) {
4626
4626
  return value2 === null || ["string", "number", "boolean"].includes(typeof value2);
4627
4627
  }
4628
- function indent(text2) {
4629
- return text2.split("\n").map((line2) => ` ${line2}`).join("\n");
4628
+ function indent(text3) {
4629
+ return text3.split("\n").map((line2) => ` ${line2}`).join("\n");
4630
4630
  }
4631
4631
  function text(value2) {
4632
4632
  return clean(isScalar(value2) ? String(value2) : JSON.stringify(value2));
@@ -4807,7 +4807,7 @@ function orgLabel(org) {
4807
4807
  }
4808
4808
 
4809
4809
  // src/program.ts
4810
- import { Command, CommanderError } from "commander";
4810
+ import { Command, CommanderError, Option as Flag } from "commander";
4811
4811
 
4812
4812
  // src/args.ts
4813
4813
  var UsageError = class extends Error {
@@ -4838,7 +4838,17 @@ var BUILTINS = [
4838
4838
  usage: "asteroid use [org]",
4839
4839
  arg: "org",
4840
4840
  description: "Make an organization, by id or name, the default for later commands. Without one, pick from a list.",
4841
- args: 1
4841
+ args: 1,
4842
+ hiddenOptions: [
4843
+ [
4844
+ "--reason <why>",
4845
+ "Staff only. Why you need access to an organization you are not a member of, such as a ticket id. It is audited. Asked for in a terminal."
4846
+ ],
4847
+ [
4848
+ "--exit",
4849
+ "Staff only. Revoke every staff token now and go back to your own organization."
4850
+ ]
4851
+ ]
4842
4852
  },
4843
4853
  {
4844
4854
  command: "complete",
@@ -4875,8 +4885,8 @@ function collect(value2, previous) {
4875
4885
  return [...previous ?? [], value2];
4876
4886
  }
4877
4887
  function optionText(option2) {
4878
- const text2 = option2.description?.replace(/\s+/g, " ").trim() ?? "";
4879
- const parts = [text2 && !/[.!?]$/.test(text2) ? `${text2}.` : text2];
4888
+ const text3 = option2.description?.replace(/\s+/g, " ").trim() ?? "";
4889
+ const parts = [text3 && !/[.!?]$/.test(text3) ? `${text3}.` : text3];
4880
4890
  if (option2.enum) parts.push(`One of: ${option2.enum.join(", ")}.`);
4881
4891
  if (option2.type.endsWith("[]")) parts.push("Repeatable.");
4882
4892
  if (option2.required) parts.push("Required.");
@@ -4961,12 +4971,14 @@ function defineCommands(root, options, dotted = /* @__PURE__ */ new Map(), onInv
4961
4971
  "after",
4962
4972
  "\nRun `asteroid --version` to print the CLI version."
4963
4973
  );
4964
- for (const [flags, text2] of GLOBAL_FLAGS) {
4965
- if (!flags.startsWith("-h")) root.option(flags, text2);
4974
+ for (const [flags, text3] of GLOBAL_FLAGS) {
4975
+ if (!flags.startsWith("-h")) root.option(flags, text3);
4966
4976
  }
4967
4977
  for (const builtin of options.builtins ?? []) {
4968
4978
  const leaf = root.command(builtin.command).description(builtin.description);
4969
4979
  if (builtin.arg) leaf.argument(`[${builtin.arg}]`);
4980
+ for (const [flags, text3] of builtin.hiddenOptions ?? [])
4981
+ leaf.addOption(new Flag(flags, text3).hideHelp());
4970
4982
  leaf.action((...args) => {
4971
4983
  const cmd = args.at(-1);
4972
4984
  onInvocation({
@@ -5055,8 +5067,8 @@ async function parseInvocation(argv2, options) {
5055
5067
  }
5056
5068
  const { argv: rest, dotted } = splitDotted(argv2);
5057
5069
  const root = new Command("asteroid").exitOverride().helpCommand(false).configureOutput({
5058
- writeOut: (text2) => stdout += text2,
5059
- writeErr: (text2) => stderr += text2
5070
+ writeOut: (text3) => stdout += text3,
5071
+ writeErr: (text3) => stderr += text3
5060
5072
  });
5061
5073
  defineCommands(root, options, dotted, (invocation) => {
5062
5074
  result = invocation;
@@ -5210,11 +5222,12 @@ function tokenClaims(token) {
5210
5222
  var SERVICE = "asteroid-cli";
5211
5223
  var LOCK_STALE_MS = 3e4;
5212
5224
  var ACCOUNT = "refresh-token";
5213
- async function keyring() {
5225
+ var STAFF_ACCOUNT = "staff-tokens";
5226
+ async function keyring(account = ACCOUNT) {
5214
5227
  if (process.env.ASTEROID_NO_KEYCHAIN) return void 0;
5215
5228
  try {
5216
5229
  const { Entry } = await import("@napi-rs/keyring");
5217
- const entry = new Entry(SERVICE, ACCOUNT);
5230
+ const entry = new Entry(SERVICE, account);
5218
5231
  entry.getPassword();
5219
5232
  return {
5220
5233
  get: () => entry.getPassword() ?? null,
@@ -5254,6 +5267,9 @@ var CredentialStore = class {
5254
5267
  get tokenFile() {
5255
5268
  return join2(this.env.configDir, "tokens.json");
5256
5269
  }
5270
+ get staffFile() {
5271
+ return join2(this.env.configDir, "staff-tokens.json");
5272
+ }
5257
5273
  async readLogin() {
5258
5274
  const ring = await keyring();
5259
5275
  const raw = ring ? tryGet(ring).value : null;
@@ -5282,22 +5298,95 @@ var CredentialStore = class {
5282
5298
  const cleared = current.ok && (!current.value || trySet(ring, null));
5283
5299
  rmSync(this.loginFile, { force: true });
5284
5300
  rmSync(this.tokenFile, { force: true });
5285
- return cleared;
5301
+ const staffCleared = await this.withStaffLock(
5302
+ () => this.writeStaffTokens({})
5303
+ );
5304
+ return staffCleared && cleared;
5305
+ }
5306
+ /** Every staff token held, expired ones included. */
5307
+ async staffTokens() {
5308
+ const ring = await keyring(STAFF_ACCOUNT);
5309
+ const raw = ring ? tryGet(ring).value : null;
5310
+ const tokens = raw ? JSON.parse(raw) : readJson(this.staffFile, {});
5311
+ return Object.values(tokens);
5312
+ }
5313
+ /** The staff token for an org, matched by WorkOS id, id or name. */
5314
+ async staffToken(ref) {
5315
+ const lower = ref.toLowerCase();
5316
+ return (await this.staffTokens()).find(
5317
+ ({ org }) => org.workosId.toLowerCase() === lower || org.id?.toLowerCase() === lower || org.name?.toLowerCase() === lower
5318
+ );
5319
+ }
5320
+ /** Saves a staff token and drops expired ones. `replaces` is dropped too. */
5321
+ async saveStaffToken(token, replaces) {
5322
+ await this.withStaffLock(async () => {
5323
+ const tokens = {};
5324
+ for (const held of await this.staffTokens())
5325
+ if (held.expiresAt > Date.now() && held.grantId !== replaces)
5326
+ tokens[held.org.workosId] = held;
5327
+ tokens[token.org.workosId] = token;
5328
+ if (!await this.writeStaffTokens(tokens))
5329
+ throw new WorkosError(
5330
+ "keychain",
5331
+ "could not save the staff token in the OS keychain. Unlock it and try again."
5332
+ );
5333
+ });
5334
+ }
5335
+ async dropStaffToken(grantId) {
5336
+ await this.withStaffLock(async () => {
5337
+ const tokens = {};
5338
+ for (const held of await this.staffTokens())
5339
+ if (held.grantId !== grantId) tokens[held.org.workosId] = held;
5340
+ if (!await this.writeStaffTokens(tokens))
5341
+ throw new WorkosError(
5342
+ "keychain",
5343
+ "could not remove the staff token from the OS keychain. Unlock it and try again."
5344
+ );
5345
+ });
5346
+ }
5347
+ async writeStaffTokens(tokens) {
5348
+ const empty = Object.keys(tokens).length === 0;
5349
+ const ring = await keyring(STAFF_ACCOUNT);
5350
+ if (ring && trySet(ring, empty ? null : JSON.stringify(tokens))) {
5351
+ rmSync(this.staffFile, { force: true });
5352
+ return true;
5353
+ }
5354
+ const stale = ring ? tryGet(ring) : { ok: true, value: null };
5355
+ if (!stale.ok || stale.value && !trySet(ring, null)) return false;
5356
+ if (empty) rmSync(this.staffFile, { force: true });
5357
+ else writePrivateJson(this.staffFile, tokens);
5358
+ return true;
5286
5359
  }
5287
5360
  async withRefreshLock(work) {
5361
+ return this.withLock(
5362
+ "refresh",
5363
+ "another asteroid command is refreshing the sign-in.",
5364
+ work
5365
+ );
5366
+ }
5367
+ /** Two commands at once must not lose a grant they could still revoke. */
5368
+ async withStaffLock(work) {
5369
+ return this.withLock(
5370
+ "staff",
5371
+ "another asteroid command is saving staff access.",
5372
+ work
5373
+ );
5374
+ }
5375
+ async withLock(name, busy, work) {
5288
5376
  mkdirSync2(this.env.configDir, { recursive: true, mode: 448 });
5289
- const lockfilePath = join2(this.env.configDir, "refresh.lock");
5377
+ const lockfilePath = join2(this.env.configDir, `${name}.lock`);
5290
5378
  let release;
5291
5379
  try {
5292
- release = await lockfile.lock(this.env.configDir, {
5380
+ release = await lockfile.lock(join2(this.env.configDir, name), {
5293
5381
  lockfilePath,
5382
+ realpath: false,
5294
5383
  stale: LOCK_STALE_MS,
5295
5384
  retries: { retries: 60, minTimeout: 100, maxTimeout: 500 }
5296
5385
  });
5297
5386
  } catch {
5298
5387
  throw new WorkosError(
5299
5388
  "locked",
5300
- `another asteroid command is refreshing the sign-in. Try again, or delete ${lockfilePath} if none is running.`
5389
+ `${busy} Try again, or delete ${lockfilePath} if none is running.`
5301
5390
  );
5302
5391
  }
5303
5392
  try {
@@ -5324,8 +5413,25 @@ var CredentialStore = class {
5324
5413
  // src/auth/credentials.ts
5325
5414
  var AuthError = class extends Error {
5326
5415
  };
5416
+ var NotMemberError = class extends AuthError {
5417
+ };
5327
5418
  var API_KEY_OVERRIDES = "ASTEROID_API_KEY is set, so the CLI uses that key and ignores `asteroid login`.";
5328
5419
  var UNSET_API_KEY = "Run `unset ASTEROID_API_KEY` to use your login instead.";
5420
+ function isStaffLogin(login2) {
5421
+ return login2?.email?.toLowerCase().endsWith("@asteroid.ai") ?? false;
5422
+ }
5423
+ function staffHint(ref, staff) {
5424
+ return staff ? ` Asteroid staff: run \`asteroid use ${clean(ref)} --reason <why>\`.` : "";
5425
+ }
5426
+ function staffAuth(held) {
5427
+ return {
5428
+ kind: "bearer",
5429
+ headers: { Authorization: `Bearer ${held.token}` },
5430
+ org: held.org,
5431
+ expiresAt: held.expiresAt,
5432
+ staff: { grantId: held.grantId, reason: held.reason }
5433
+ };
5434
+ }
5329
5435
  function orgFromToken(token) {
5330
5436
  const claims = tokenClaims(token);
5331
5437
  if (!claims.org_id) return void 0;
@@ -5352,7 +5458,7 @@ function bearer(token, expiresAt) {
5352
5458
  };
5353
5459
  }
5354
5460
  var SSO_OR_MFA = /^(sso_required|mfa_.*|organization_authentication_methods_required)$/;
5355
- function refreshError(error, org) {
5461
+ function refreshError(error, org, login2) {
5356
5462
  if (error.code === "invalid_grant") {
5357
5463
  return new AuthError("your sign-in has expired. Run `asteroid login`.");
5358
5464
  }
@@ -5362,8 +5468,8 @@ function refreshError(error, org) {
5362
5468
  );
5363
5469
  }
5364
5470
  if (error.code === "organization_membership_not_found") {
5365
- return new AuthError(
5366
- `you are not a member of ${orgLabel(org)}. Staff access to other organizations is not available yet.`
5471
+ return new NotMemberError(
5472
+ `you are not a member of ${orgLabel(org)}.${staffHint(org.id ?? org.workosId, isStaffLogin(login2))}`
5367
5473
  );
5368
5474
  }
5369
5475
  return new AuthError(
@@ -5374,15 +5480,24 @@ async function resolveAuth(env, config, orgRef, fetchImpl = fetch) {
5374
5480
  if (env.apiKey)
5375
5481
  return { kind: "api-key", headers: { "X-Api-Key": env.apiKey } };
5376
5482
  const store = new CredentialStore(env);
5377
- if (!await store.readLogin()) {
5483
+ const login2 = await store.readLogin();
5484
+ if (!login2) {
5378
5485
  throw new AuthError(
5379
5486
  "not signed in. Run `asteroid login`, or set ASTEROID_API_KEY."
5380
5487
  );
5381
5488
  }
5489
+ const target = orgRef ?? config.org?.workosId;
5490
+ const held = target ? await store.staffToken(target) : void 0;
5491
+ if (held) {
5492
+ if (held.expiresAt - 3e4 > Date.now()) return staffAuth(held);
5493
+ throw new AuthError(
5494
+ `your staff access to ${orgLabel(held.org)} has expired.${staffHint(held.org.id ?? held.org.workosId, true)}`
5495
+ );
5496
+ }
5382
5497
  const org = orgRef ? findOrg(orgRef, config.orgs ?? []) : config.org;
5383
5498
  if (orgRef && !org) {
5384
5499
  throw new AuthError(
5385
- `unknown organization ${orgRef}. Run \`asteroid orgs\` to list yours.`
5500
+ `unknown organization ${clean(orgRef)}. Run \`asteroid orgs\` to list yours.${staffHint(orgRef, isStaffLogin(login2))}`
5386
5501
  );
5387
5502
  }
5388
5503
  if (!org) {
@@ -5395,8 +5510,8 @@ async function resolveAuth(env, config, orgRef, fetchImpl = fetch) {
5395
5510
  return store.withRefreshLock(async () => {
5396
5511
  const fresh = store.cachedToken(org.workosId);
5397
5512
  if (fresh) return bearer(fresh.accessToken, fresh.expiresAt);
5398
- const login2 = await store.readLogin();
5399
- if (!login2) {
5513
+ const login3 = await store.readLogin();
5514
+ if (!login3) {
5400
5515
  throw new AuthError("not signed in. Run `asteroid login`.");
5401
5516
  }
5402
5517
  try {
@@ -5406,10 +5521,10 @@ async function resolveAuth(env, config, orgRef, fetchImpl = fetch) {
5406
5521
  apiUrl: env.workosApiUrl,
5407
5522
  fetch: fetchImpl
5408
5523
  },
5409
- login2.refreshToken,
5524
+ login3.refreshToken,
5410
5525
  org.workosId
5411
5526
  );
5412
- await store.writeLogin({ ...login2, refreshToken: session2.refreshToken });
5527
+ await store.writeLogin({ ...login3, refreshToken: session2.refreshToken });
5413
5528
  const expiresAt = (tokenClaims(session2.accessToken).exp ?? 0) * 1e3 || Date.now() + 6e4;
5414
5529
  const auth = bearer(session2.accessToken, expiresAt);
5415
5530
  store.cacheToken(auth.org?.workosId ?? org.workosId, {
@@ -5419,7 +5534,7 @@ async function resolveAuth(env, config, orgRef, fetchImpl = fetch) {
5419
5534
  rememberOrg(env, config, auth.org);
5420
5535
  return auth;
5421
5536
  } catch (error) {
5422
- if (error instanceof WorkosError) throw refreshError(error, org);
5537
+ if (error instanceof WorkosError) throw refreshError(error, org, login3);
5423
5538
  throw error;
5424
5539
  }
5425
5540
  });
@@ -5435,7 +5550,7 @@ var ApiError = class extends Error {
5435
5550
  status;
5436
5551
  body;
5437
5552
  };
5438
- var VERSION = "1.9.12";
5553
+ var VERSION = "1.9.27";
5439
5554
  function problem(status, body, fallback) {
5440
5555
  if (body && typeof body === "object") {
5441
5556
  const record2 = body;
@@ -5495,14 +5610,14 @@ async function send(apiUrl, auth, request, binary, fetchImpl = fetch) {
5495
5610
  }
5496
5611
  const contentType = response.headers.get("content-type") ?? "";
5497
5612
  if (!response.ok) {
5498
- const text2 = await response.text();
5499
- let parsed = text2;
5613
+ const text3 = await response.text();
5614
+ let parsed = text3;
5500
5615
  try {
5501
- parsed = JSON.parse(text2);
5616
+ parsed = JSON.parse(text3);
5502
5617
  } catch {
5503
- parsed = text2;
5618
+ parsed = text3;
5504
5619
  }
5505
- let message = problem(response.status, parsed, text2.slice(0, 200));
5620
+ let message = problem(response.status, parsed, text3.slice(0, 200));
5506
5621
  const sentKey = "X-Api-Key" in hopHeaders;
5507
5622
  if (sentKey && response.status === 401)
5508
5623
  message = `ASTEROID_API_KEY was rejected: ${message}. Check the key. ${UNSET_API_KEY}`;
@@ -5614,11 +5729,21 @@ async function orgs(deps, flagOrg, json) {
5614
5729
  deps.fetch
5615
5730
  );
5616
5731
  const list = loadConfig(deps.env).orgs ?? [];
5732
+ const staff = (await new CredentialStore(deps.env).staffTokens()).filter(
5733
+ (held) => held.expiresAt > Date.now()
5734
+ );
5617
5735
  const active = auth.org?.workosId;
5618
5736
  if (json) {
5619
5737
  deps.stdout(
5620
5738
  `${JSON.stringify(
5621
- list.map((org) => ({ ...org, active: org.workosId === active })),
5739
+ [
5740
+ ...list.map((org) => ({ ...org, active: org.workosId === active })),
5741
+ ...staff.map((held) => ({
5742
+ ...held.org,
5743
+ active: held.org.workosId === active,
5744
+ staff: { reason: held.reason, expiresAt: held.expiresAt }
5745
+ }))
5746
+ ],
5622
5747
  null,
5623
5748
  2
5624
5749
  )}
@@ -5629,13 +5754,204 @@ async function orgs(deps, flagOrg, json) {
5629
5754
  for (const org of list)
5630
5755
  deps.stdout(`${org.workosId === active ? "*" : " "} ${orgLabel(org)}
5631
5756
  `);
5757
+ for (const held of staff)
5758
+ deps.stdout(
5759
+ `${held.org.workosId === active ? "*" : " "} ${orgLabel(held.org)} staff access, ${timeLeft(held.expiresAt)} left
5760
+ `
5761
+ );
5762
+ return 0;
5763
+ }
5764
+ function timeLeft(expiresAt) {
5765
+ const minutes = Math.max(0, Math.round((expiresAt - Date.now()) / 6e4));
5766
+ return minutes >= 60 ? `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, "0")}m` : `${minutes}m`;
5767
+ }
5768
+ function isMember(config, org) {
5769
+ return (config.orgs ?? []).some((o) => o.workosId === org.workosId);
5770
+ }
5771
+ function homeOrg(config) {
5772
+ const org = config.org && isMember(config, config.org) ? config.org : config.orgs?.[0];
5773
+ if (!org)
5774
+ throw new AuthError(
5775
+ "staff access needs a sign-in to one of your own organizations. Run `asteroid login`."
5776
+ );
5777
+ return org;
5778
+ }
5779
+ var STAFF_TOKEN_PATH = "/users/v2/staff-tokens";
5780
+ async function mintStaffToken(deps, config, ref, reason) {
5781
+ const home = await resolveAuth(
5782
+ deps.env,
5783
+ config,
5784
+ homeOrg(config).workosId,
5785
+ deps.fetch
5786
+ );
5787
+ let minted;
5788
+ try {
5789
+ const response = await send(
5790
+ deps.env.apiUrl,
5791
+ home,
5792
+ {
5793
+ method: "POST",
5794
+ path: STAFF_TOKEN_PATH,
5795
+ query: new URLSearchParams(),
5796
+ json: { organization: ref, reason }
5797
+ },
5798
+ false,
5799
+ deps.fetch
5800
+ );
5801
+ minted = response.json;
5802
+ } catch (error) {
5803
+ if (error instanceof ApiError && error.status === 403)
5804
+ throw new AuthError(
5805
+ `you are not a member of ${clean(ref)}, and staff access is for Asteroid staff only.`
5806
+ );
5807
+ if (error instanceof ApiError && error.status === 404)
5808
+ throw new UsageError(
5809
+ `unknown organization ${clean(ref)}. Pass its id or its WorkOS id (org_...).`
5810
+ );
5811
+ throw error;
5812
+ }
5813
+ return {
5814
+ token: minted.token,
5815
+ grantId: minted.grantId,
5816
+ org: {
5817
+ workosId: minted.workosOrganizationId ?? minted.organizationId,
5818
+ id: minted.organizationId,
5819
+ name: minted.organizationName
5820
+ },
5821
+ reason: minted.reason,
5822
+ expiresAt: Date.parse(minted.expiresAt)
5823
+ };
5824
+ }
5825
+ async function revokeStaffToken(deps, home, held) {
5826
+ try {
5827
+ await send(
5828
+ deps.env.apiUrl,
5829
+ home,
5830
+ {
5831
+ method: "DELETE",
5832
+ path: `${STAFF_TOKEN_PATH}/${encodeURIComponent(held.grantId)}`,
5833
+ query: new URLSearchParams()
5834
+ },
5835
+ false,
5836
+ deps.fetch
5837
+ );
5838
+ return true;
5839
+ } catch (error) {
5840
+ if (error instanceof ApiError && error.status === 404) return true;
5841
+ deps.stderr(
5842
+ `Could not revoke staff access to ${orgLabel(held.org)}: ${clean(error instanceof Error ? error.message : String(error))}. It ends at ${new Date(held.expiresAt).toISOString()}.
5843
+ `
5844
+ );
5845
+ return false;
5846
+ }
5847
+ }
5848
+ async function exitStaff(deps) {
5849
+ const store = new CredentialStore(deps.env);
5850
+ const held = await store.staffTokens();
5851
+ const config = loadConfig(deps.env);
5852
+ if (held.length === 0) {
5853
+ deps.stderr("No staff access to leave.\n");
5854
+ return 0;
5855
+ }
5856
+ const home = await resolveAuth(
5857
+ deps.env,
5858
+ config,
5859
+ homeOrg(config).workosId,
5860
+ deps.fetch
5861
+ );
5862
+ const left = [];
5863
+ const kept = [];
5864
+ for (const token of held) {
5865
+ const ended = token.expiresAt <= Date.now() || await revokeStaffToken(deps, home, token);
5866
+ if (!ended) {
5867
+ kept.push(token);
5868
+ continue;
5869
+ }
5870
+ await store.dropStaffToken(token.grantId);
5871
+ left.push(token);
5872
+ }
5873
+ const back = held.find((t) => t.from)?.from ?? homeOrg(config);
5874
+ saveConfig(deps.env, { ...loadConfig(deps.env), org: back });
5875
+ if (left.length > 0)
5876
+ deps.stderr(
5877
+ `Left staff access to ${left.map((t) => orgLabel(t.org)).join(", ")}.
5878
+ `
5879
+ );
5880
+ if (kept.length > 0)
5881
+ deps.stderr(
5882
+ `Still holding staff access to ${kept.map((t) => orgLabel(t.org)).join(", ")}. Run \`asteroid use --exit\` again to retry.
5883
+ `
5884
+ );
5885
+ deps.stderr(`Now using ${orgLabel(back)}.
5886
+ `);
5887
+ return kept.length === 0 ? 0 : 1;
5888
+ }
5889
+ async function askReason(deps, org) {
5890
+ if (!deps.ui) return void 0;
5891
+ const reason = await deps.ui.text(
5892
+ `You are not a member of ${clean(org)}. Why do you need staff access? This is audited.`
5893
+ );
5894
+ return reason?.trim() || void 0;
5895
+ }
5896
+ async function useAsStaff(deps, config, ref, reason, staff) {
5897
+ const store = new CredentialStore(deps.env);
5898
+ const held = await store.staffToken(ref);
5899
+ if (held && held.expiresAt - 3e4 > Date.now() && !reason) {
5900
+ saveConfig(deps.env, { ...config, org: held.org });
5901
+ deps.stderr(
5902
+ `Now using ${orgLabel(held.org)} with staff access, ${timeLeft(held.expiresAt)} left.
5903
+ `
5904
+ );
5905
+ return 0;
5906
+ }
5907
+ if (!reason && staff) reason = await askReason(deps, ref);
5908
+ if (!reason)
5909
+ throw new UsageError(
5910
+ `you are not a member of ${clean(ref)}.${staffHint(ref, staff)}${staff ? " The reason is audited." : ""}`
5911
+ );
5912
+ const minted = await mintStaffToken(
5913
+ deps,
5914
+ config,
5915
+ held?.org.id ?? ref,
5916
+ reason
5917
+ );
5918
+ const previous = config.org ? isMember(config, config.org) ? config.org : (await store.staffToken(config.org.workosId))?.from : void 0;
5919
+ try {
5920
+ await store.saveStaffToken(
5921
+ { ...minted, from: previous ?? held?.from },
5922
+ held?.grantId
5923
+ );
5924
+ } catch (error) {
5925
+ const home = await resolveAuth(
5926
+ deps.env,
5927
+ config,
5928
+ homeOrg(config).workosId,
5929
+ deps.fetch
5930
+ );
5931
+ await revokeStaffToken(deps, home, minted);
5932
+ throw error;
5933
+ }
5934
+ saveConfig(deps.env, { ...loadConfig(deps.env), org: minted.org });
5935
+ deps.stderr(
5936
+ `Staff access to ${orgLabel(minted.org)} for ${timeLeft(minted.expiresAt)}, until ${new Date(minted.expiresAt).toLocaleTimeString()}. Reason: ${clean(minted.reason)}. This is audited. Run \`asteroid use --exit\` to leave.
5937
+ `
5938
+ );
5632
5939
  return 0;
5633
5940
  }
5634
- async function use(deps, ref) {
5941
+ async function use(deps, ref, options = {}) {
5635
5942
  if (deps.env.apiKey)
5636
5943
  throw new UsageError(
5637
5944
  `${API_KEY_OVERRIDES} \`use\` only switches the login's organization. ${UNSET_API_KEY}`
5638
5945
  );
5946
+ if (options.exit) {
5947
+ if (ref || options.reason)
5948
+ throw new UsageError("usage: asteroid use --exit");
5949
+ return exitStaff(deps);
5950
+ }
5951
+ if (options.reason?.startsWith("-"))
5952
+ throw new UsageError(
5953
+ `--reason needs a value, not ${clean(options.reason)}. Give the reason without a leading -.`
5954
+ );
5639
5955
  const config = loadConfig(deps.env);
5640
5956
  if (!ref && deps.ui && (config.orgs?.length ?? 0) > 0) {
5641
5957
  ref = await deps.ui.select(
@@ -5648,12 +5964,20 @@ async function use(deps, ref) {
5648
5964
  if (!ref) return 1;
5649
5965
  }
5650
5966
  if (!ref) throw new UsageError("usage: asteroid use <org>");
5967
+ const login2 = await new CredentialStore(deps.env).readLogin();
5968
+ if (!login2) throw new AuthError("not signed in. Run `asteroid login`.");
5969
+ const staff = isStaffLogin(login2);
5651
5970
  const org = findOrg(ref, config.orgs ?? []);
5652
- if (!org)
5653
- throw new UsageError(
5654
- `unknown organization ${ref}. Run \`asteroid orgs\` to list yours.`
5655
- );
5656
- const auth = await resolveAuth(deps.env, config, org.workosId, deps.fetch);
5971
+ if (!org || !isMember(config, org) && options.reason)
5972
+ return useAsStaff(deps, config, ref, options.reason, staff);
5973
+ let auth;
5974
+ try {
5975
+ auth = await resolveAuth(deps.env, config, org.workosId, deps.fetch);
5976
+ } catch (error) {
5977
+ if (error instanceof NotMemberError)
5978
+ return useAsStaff(deps, config, ref, options.reason, staff);
5979
+ throw error;
5980
+ }
5657
5981
  const updated = loadConfig(deps.env);
5658
5982
  updated.org = updated.orgs?.find((o) => o.workosId === org.workosId) ?? auth.org ?? org;
5659
5983
  saveConfig(deps.env, updated);
@@ -5685,6 +6009,7 @@ async function whoami(deps, flagOrg, json) {
5685
6009
  );
5686
6010
  return 0;
5687
6011
  }
6012
+ const grant = body.staffGrant;
5688
6013
  const person = body.person;
5689
6014
  const orgId = body.organizationId;
5690
6015
  const organization = auth.org ? orgLabel(auth.org) : orgId ?? "none";
@@ -5693,9 +6018,10 @@ async function whoami(deps, flagOrg, json) {
5693
6018
  as: person?.email ?? body.id,
5694
6019
  kind: body.kind,
5695
6020
  credential: body.credential,
5696
- source: auth.kind === "api-key" ? "ASTEROID_API_KEY" : "asteroid login",
6021
+ source: auth.kind === "api-key" ? "ASTEROID_API_KEY" : auth.staff ? "asteroid use (staff access)" : "asteroid login",
5697
6022
  organization,
5698
- ...minutes === void 0 ? {} : { tokenExpiresIn: `${minutes}m` }
6023
+ ...grant ? { staffReason: grant.reason } : {},
6024
+ ...auth.expiresAt === void 0 ? {} : { tokenExpiresIn: timeLeft(auth.expiresAt) }
5699
6025
  })}
5700
6026
  `
5701
6027
  );
@@ -6089,8 +6415,8 @@ function executionDir(options, execution) {
6089
6415
  );
6090
6416
  return join3(root, "executions", executionDirName(execution.id));
6091
6417
  }
6092
- async function writeText(sync, path, text2) {
6093
- await sync.files.write(path, Buffer.from(text2, "utf8"));
6418
+ async function writeText(sync, path, text3) {
6419
+ await sync.files.write(path, Buffer.from(text3, "utf8"));
6094
6420
  }
6095
6421
  async function writeActivities(sync, dir, execution) {
6096
6422
  try {
@@ -6338,9 +6664,9 @@ async function removeStale(sync, dir, keep) {
6338
6664
  }
6339
6665
  }
6340
6666
  async function writeManifest(sync, dir, manifest) {
6341
- const text2 = `${JSON.stringify(manifest, null, 2)}
6667
+ const text3 = `${JSON.stringify(manifest, null, 2)}
6342
6668
  `;
6343
- await sync.files.write(join3(dir, MANIFEST), Buffer.from(text2, "utf8"));
6669
+ await sync.files.write(join3(dir, MANIFEST), Buffer.from(text3, "utf8"));
6344
6670
  }
6345
6671
  async function pullWorkflow(sync, options) {
6346
6672
  const request = {
@@ -6595,8 +6921,8 @@ async function runSync(command, parsed, deps) {
6595
6921
  remote: deps.remote,
6596
6922
  files: deps.files ?? nodeFiles(deps.cwd)
6597
6923
  };
6598
- const { result, text: text2 } = await execute(command, parsed, sync, deps);
6599
- deps.stdout(`${parsed.json ? JSON.stringify(result, null, 2) : text2}
6924
+ const { result, text: text3 } = await execute(command, parsed, sync, deps);
6925
+ deps.stdout(`${parsed.json ? JSON.stringify(result, null, 2) : text3}
6600
6926
  `);
6601
6927
  return 0;
6602
6928
  }
@@ -6624,7 +6950,10 @@ async function runBuiltin(name, parsed, deps) {
6624
6950
  "run `asteroid complete <shell>` to print a completion script"
6625
6951
  );
6626
6952
  case "use":
6627
- return use(deps, parsed.positionals[0]);
6953
+ return use(deps, parsed.positionals[0], {
6954
+ reason: parsed.flags.get("reason")?.[0],
6955
+ exit: parsed.flags.has("exit")
6956
+ });
6628
6957
  default:
6629
6958
  return whoami(deps, parsed.org, parsed.json);
6630
6959
  }
@@ -6653,7 +6982,8 @@ async function authFor(parsed, deps) {
6653
6982
  function announce(command, auth, deps) {
6654
6983
  if (command.readOnly) return;
6655
6984
  const org = auth.kind === "api-key" ? "the API key's organization" : auth.org ? orgLabel(auth.org) : "your default organization";
6656
- deps.stderr(`Acting in ${org}.
6985
+ const staff = auth.staff ? ` with staff access (${clean(auth.staff.reason)})` : "";
6986
+ deps.stderr(`Acting in ${org}${staff}.
6657
6987
  `);
6658
6988
  }
6659
6989
  async function runCommand(command, parsed, deps) {
@@ -6769,6 +7099,10 @@ var clackUi = {
6769
7099
  const value2 = await clack.select({ message, options: choices });
6770
7100
  return clack.isCancel(value2) ? void 0 : value2;
6771
7101
  },
7102
+ async text(message) {
7103
+ const value2 = await clack.text({ message });
7104
+ return clack.isCancel(value2) ? void 0 : value2;
7105
+ },
6772
7106
  async wait(message, done, work) {
6773
7107
  const spinner2 = clack.spinner();
6774
7108
  spinner2.start(message);
@@ -6803,8 +7137,8 @@ var code = await run(argv, {
6803
7137
  env: readEnv(),
6804
7138
  cwd: process.cwd(),
6805
7139
  fetch,
6806
- stdout: (text2) => process.stdout.write(text2),
6807
- stderr: (text2) => process.stderr.write(text2),
7140
+ stdout: (text3) => process.stdout.write(text3),
7141
+ stderr: (text3) => process.stderr.write(text3),
6808
7142
  writeBytes: (bytes) => process.stdout.write(bytes),
6809
7143
  ui: process.stdin.isTTY && process.stderr.isTTY ? clackUi : void 0,
6810
7144
  stdoutIsTTY: process.stdout.isTTY === true,