@asteroidai/cli 1.9.12 → 1.9.28
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 +387 -53
- package/dist/core.js +14 -2
- package/package.json +3 -8
- package/src/account.ts +0 -224
- package/src/args.ts +0 -39
- package/src/auth/credentials.ts +0 -141
- package/src/auth/store.ts +0 -145
- package/src/auth/workos.ts +0 -171
- package/src/commands.ts +0 -13
- package/src/config.ts +0 -125
- package/src/core.ts +0 -41
- package/src/generated/commands.ts +0 -4772
- package/src/http.ts +0 -115
- package/src/main.ts +0 -49
- package/src/output.test.ts +0 -71
- package/src/output.ts +0 -176
- package/src/program.test.ts +0 -155
- package/src/program.ts +0 -291
- package/src/reference.ts +0 -183
- package/src/request.test.ts +0 -164
- package/src/request.ts +0 -252
- package/src/run.test.ts +0 -431
- package/src/run.ts +0 -229
- package/src/sync/activities.ts +0 -80
- package/src/sync/commands.ts +0 -63
- package/src/sync/execution-pull.ts +0 -273
- package/src/sync/node-files.ts +0 -47
- package/src/sync/paths.ts +0 -45
- package/src/sync/run.ts +0 -136
- package/src/sync/sync.test.ts +0 -502
- package/src/sync/types.ts +0 -58
- package/src/sync/workflow-pull.ts +0 -185
- package/src/sync/workflow-push.ts +0 -161
- package/src/types.ts +0 -46
- package/src/ui.ts +0 -30
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(
|
|
4620
|
-
return
|
|
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(
|
|
4629
|
-
return
|
|
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
|
|
4879
|
-
const parts = [
|
|
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,
|
|
4965
|
-
if (!flags.startsWith("-h")) root.option(flags,
|
|
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: (
|
|
5059
|
-
writeErr: (
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
5366
|
-
`you are not a member of ${orgLabel(org)}.
|
|
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
|
-
|
|
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
|
|
5399
|
-
if (!
|
|
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
|
-
|
|
5524
|
+
login3.refreshToken,
|
|
5410
5525
|
org.workosId
|
|
5411
5526
|
);
|
|
5412
|
-
await store.writeLogin({ ...
|
|
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.
|
|
5553
|
+
var VERSION = "1.9.28";
|
|
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
|
|
5499
|
-
let parsed =
|
|
5613
|
+
const text3 = await response.text();
|
|
5614
|
+
let parsed = text3;
|
|
5500
5615
|
try {
|
|
5501
|
-
parsed = JSON.parse(
|
|
5616
|
+
parsed = JSON.parse(text3);
|
|
5502
5617
|
} catch {
|
|
5503
|
-
parsed =
|
|
5618
|
+
parsed = text3;
|
|
5504
5619
|
}
|
|
5505
|
-
let message = problem(response.status, parsed,
|
|
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
|
-
|
|
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
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
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
|
-
...
|
|
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,
|
|
6093
|
-
await sync.files.write(path, Buffer.from(
|
|
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
|
|
6667
|
+
const text3 = `${JSON.stringify(manifest, null, 2)}
|
|
6342
6668
|
`;
|
|
6343
|
-
await sync.files.write(join3(dir, MANIFEST), Buffer.from(
|
|
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:
|
|
6599
|
-
deps.stdout(`${parsed.json ? JSON.stringify(result, null, 2) :
|
|
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
|
-
|
|
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: (
|
|
6807
|
-
stderr: (
|
|
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,
|