@antasphere/chassis-cli 1.0.1 → 1.1.0
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/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +36 -18
- package/dist/commands/auth.js.map +1 -1
- package/dist/context.d.ts +8 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +89 -41
- package/dist/context.js.map +1 -1
- package/dist/identity.d.ts +8 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +8 -0
- package/dist/identity.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/auth.ts +39 -19
- package/src/context.ts +109 -49
- package/src/identity.ts +9 -0
- package/test/suite/hub-selection.test.ts +588 -0
- package/test/suite/login.test.ts +3 -1
- package/test/suite/profiles.test.ts +6 -1
- package/test/suite/recovery.test.ts +46 -11
- package/test/suite/whoami.test.ts +12 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antasphere/chassis-cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@antasphere/cli-core": "^0.
|
|
31
|
+
"@antasphere/cli-core": "^0.6.0",
|
|
32
32
|
"commander": "^12.1.0",
|
|
33
|
-
"@antasphere/chassis-contract": "1.0
|
|
34
|
-
"@antasphere/chassis-sdk": "1.0
|
|
33
|
+
"@antasphere/chassis-contract": "1.1.0",
|
|
34
|
+
"@antasphere/chassis-sdk": "1.1.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.15.0",
|
package/src/commands/auth.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import {
|
|
3
|
-
activeHubProfile,
|
|
4
3
|
ask,
|
|
5
4
|
CliAuthClient,
|
|
6
5
|
CliAuthError,
|
|
7
6
|
CliUsageError as CoreUsageError,
|
|
8
7
|
CLOUD_PROFILE,
|
|
9
|
-
|
|
8
|
+
firstSet,
|
|
10
9
|
HUB_TOOL,
|
|
11
10
|
loadConfig as loadCoreConfig,
|
|
12
11
|
normalizeUrl,
|
|
@@ -15,10 +14,12 @@ import {
|
|
|
15
14
|
sameInstance,
|
|
16
15
|
selectProfile,
|
|
17
16
|
saveConfig as saveCoreConfig,
|
|
17
|
+
selectHubProfile,
|
|
18
18
|
type CliProfile
|
|
19
19
|
} from '@antasphere/cli-core';
|
|
20
20
|
import type { ChassisClient } from '@antasphere/chassis-sdk';
|
|
21
21
|
import { redactKey } from '../config.js';
|
|
22
|
+
import { DEFAULT_HUB_URL } from '../identity.js';
|
|
22
23
|
import {
|
|
23
24
|
CliUsageError,
|
|
24
25
|
printJson,
|
|
@@ -48,7 +49,6 @@ import type { MeResponse } from '../workspace.js';
|
|
|
48
49
|
|
|
49
50
|
/** The hub a tool CLI signs in against inline: the same variable the hub CLI reads. */
|
|
50
51
|
const HUB_URL_ENV = 'ANTASPHERE_URL';
|
|
51
|
-
const DEFAULT_HUB_URL = 'https://account.antasphere.com';
|
|
52
52
|
|
|
53
53
|
interface AuthGlobals {
|
|
54
54
|
apiUrl?: string;
|
|
@@ -109,7 +109,7 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
|
|
|
109
109
|
createClient,
|
|
110
110
|
clearConfig,
|
|
111
111
|
configPath,
|
|
112
|
-
|
|
112
|
+
connect,
|
|
113
113
|
loadConfig,
|
|
114
114
|
profileUrl,
|
|
115
115
|
removeConnectKey,
|
|
@@ -158,6 +158,10 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
|
|
|
158
158
|
// active one, else the implicit `cloud` at account.antasphere.com; the
|
|
159
159
|
// URL is the variable, else that profile's own, else the default. A
|
|
160
160
|
// stored hub URL is never repointed at the production hub.
|
|
161
|
+
// `selectHubProfile`, the exchange's pick, goes through the same
|
|
162
|
+
// `selectProfile` with the same cloud URL, so the two agree (with no
|
|
163
|
+
// variable and no active profile it names none; the write below makes
|
|
164
|
+
// this pick the active one).
|
|
161
165
|
const hubConfig = loadCoreConfig(io.env, HUB_TOOL);
|
|
162
166
|
const selected = selectProfile({
|
|
163
167
|
config: hubConfig,
|
|
@@ -283,20 +287,29 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
|
|
|
283
287
|
// stores it, so every other tool CLI is signed in too. `--email`
|
|
284
288
|
// naming ANOTHER account than the stored login signs that account in
|
|
285
289
|
// afresh, replacing the stored login (PRDCT-3032): the person who
|
|
286
|
-
// signed in with the wrong address switches from here.
|
|
287
|
-
|
|
288
|
-
|
|
290
|
+
// signed in with the wrong address switches from here. A stored key
|
|
291
|
+
// with no usable address (absent or blank) is someone unknown, so
|
|
292
|
+
// `--email` on it signs in afresh too (verifier round 1, F2).
|
|
293
|
+
let hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
|
|
294
|
+
const hasKey = Boolean(hub.profile?.apiKey);
|
|
295
|
+
const stored = hasKey ? firstSet(hub.profile?.email) : undefined;
|
|
289
296
|
const switching =
|
|
290
|
-
|
|
297
|
+
hasKey &&
|
|
298
|
+
opts.email !== undefined &&
|
|
299
|
+
(stored === undefined || opts.email.toLowerCase() !== stored.toLowerCase());
|
|
291
300
|
if (switching) {
|
|
292
|
-
io.err.write(
|
|
301
|
+
io.err.write(
|
|
302
|
+
stored
|
|
303
|
+
? `Replacing the Antasphere login ${stored} with ${opts.email}.\n`
|
|
304
|
+
: `Signing in as ${opts.email}: the stored Antasphere login carries no address.\n`
|
|
305
|
+
);
|
|
293
306
|
}
|
|
294
307
|
if (!hub.profile?.apiKey || switching) await hubLogin(opts);
|
|
295
308
|
// A login is an explicit re-sign-in (verifier round 1, F3): the key
|
|
296
309
|
// cached on this profile is revoked on its instance (best effort: a
|
|
297
310
|
// dead one answers 401) and forgotten, and the exchange runs afresh,
|
|
298
311
|
// so a stale cached key never survives a login.
|
|
299
|
-
hub =
|
|
312
|
+
hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
|
|
300
313
|
const stale = hub.name
|
|
301
314
|
? loadConfig(io.env).profiles[profileName]?.connectKeys?.[hub.name]
|
|
302
315
|
: undefined;
|
|
@@ -314,13 +327,13 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
|
|
|
314
327
|
// no longer accepts is replaced by a fresh sign-in, once.
|
|
315
328
|
let outcome;
|
|
316
329
|
try {
|
|
317
|
-
outcome = await
|
|
330
|
+
outcome = await connect(ctx, { profileName, cached: false });
|
|
318
331
|
} catch (e) {
|
|
319
332
|
if (!(e instanceof CoreUsageError) || !/rejected/.test(e.message)) throw e;
|
|
320
333
|
io.err.write('The stored Antasphere login was rejected: signing in again.\n');
|
|
321
334
|
await hubLogin(opts);
|
|
322
|
-
hub =
|
|
323
|
-
outcome = await
|
|
335
|
+
hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
|
|
336
|
+
outcome = await connect(ctx, { profileName, cached: false });
|
|
324
337
|
}
|
|
325
338
|
if (outcome.outcome === 'not_cloud') {
|
|
326
339
|
throw new CliUsageError(`${baseUrl} stopped answering as an Antasphere-cloud instance; try again.`);
|
|
@@ -369,10 +382,13 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
|
|
|
369
382
|
|
|
370
383
|
/** The hub login `--all` ends: `antasphere logout` from here (the key revoked on the hub, the profile cleared). */
|
|
371
384
|
async function hubLogout(): Promise<{ email: string | undefined; revoked: boolean } | null> {
|
|
372
|
-
const hub =
|
|
385
|
+
const hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
|
|
373
386
|
if (!hub.name || !hub.profile?.apiKey) return null;
|
|
387
|
+
// Revoked at the HUB's own URL: the profile's, else the default hub. Never
|
|
388
|
+
// the tool kit's `profileUrl`, whose fallback is the tool's cloud host: a
|
|
389
|
+
// hub profile with no baseUrl would send the hub key there.
|
|
374
390
|
const auth = new CliAuthClient({
|
|
375
|
-
baseUrl:
|
|
391
|
+
baseUrl: hub.profile.baseUrl?.trim() || DEFAULT_HUB_URL,
|
|
376
392
|
...(io.fetch ? { fetch: io.fetch } : {})
|
|
377
393
|
});
|
|
378
394
|
let revoked = false;
|
|
@@ -402,7 +418,7 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
|
|
|
402
418
|
const ctx = resolveContext(cmd, io);
|
|
403
419
|
const profileName = ctx.profileName;
|
|
404
420
|
const profile = profileName ? ctx.config.profiles[profileName] : undefined;
|
|
405
|
-
const hubLogin =
|
|
421
|
+
const hubLogin = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL }).profile;
|
|
406
422
|
if (!profileName || !profile) {
|
|
407
423
|
// Nothing of this tool's on the machine. The Antasphere login, when
|
|
408
424
|
// there is one, is the family's: it is ended here on --all, else
|
|
@@ -510,7 +526,7 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
|
|
|
510
526
|
// `me` is the answer WITH the selection applied, so its workspace is
|
|
511
527
|
// the one this command (and any other, same flags) really ran in.
|
|
512
528
|
const source = workspaceSource(ctx);
|
|
513
|
-
const hub =
|
|
529
|
+
const hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
|
|
514
530
|
const hubLine = hubLoginLine(ctx, hub.profile);
|
|
515
531
|
if (ctx.json) {
|
|
516
532
|
return printJson(io, {
|
|
@@ -600,7 +616,7 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
|
|
|
600
616
|
.action(async (_opts, cmd: Command) => {
|
|
601
617
|
const globals = cmd.optsWithGlobals() as AuthGlobals;
|
|
602
618
|
const config = loadConfig(io.env);
|
|
603
|
-
const hub =
|
|
619
|
+
const hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
|
|
604
620
|
// The implicit cloud profile is listed even before it is on disk: it is
|
|
605
621
|
// where a clean machine runs, and where `login` with no URL signs in.
|
|
606
622
|
const rows: Array<[string, CliProfile]> = Object.entries(config.profiles);
|
|
@@ -612,8 +628,12 @@ export function registerAuthCommands<TClient extends ChassisClient<string>>(
|
|
|
612
628
|
const entry = hub.name ? p.connectKeys?.[hub.name] : undefined;
|
|
613
629
|
if (entry) return { kind: 'hub', text: `via Antasphere${entry.email ? ` (${entry.email})` : ''}` };
|
|
614
630
|
const others = Object.keys(p.connectKeys ?? {});
|
|
631
|
+
// The hub profile is the one ANTASPHERE_URL names when it is set, else the active one.
|
|
632
|
+
const chosen = firstSet(io.env[HUB_URL_ENV])
|
|
633
|
+
? 'not the one ANTASPHERE_URL names'
|
|
634
|
+
: 'not the active one';
|
|
615
635
|
if (others.length > 0)
|
|
616
|
-
return { kind: 'hub', text: `via Antasphere (login "${others.join('", "')}",
|
|
636
|
+
return { kind: 'hub', text: `via Antasphere (login "${others.join('", "')}", ${chosen})` };
|
|
617
637
|
return { kind: 'none', text: '(not signed in)' };
|
|
618
638
|
};
|
|
619
639
|
if (globals.json) {
|
package/src/context.ts
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import { PlatformApiError, type ChassisClient, type ClientOptions } from '@antasphere/chassis-sdk';
|
|
2
2
|
import type { Command } from 'commander';
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
cachedConnectKey,
|
|
5
5
|
CLOUD_PROFILE,
|
|
6
6
|
CliAuthClient,
|
|
7
7
|
CliUsageError,
|
|
8
8
|
connectOnDemand,
|
|
9
|
-
|
|
9
|
+
firstSet,
|
|
10
|
+
HUB_TOOL,
|
|
11
|
+
loadConfig as loadCoreConfig,
|
|
10
12
|
printJson as corePrintJson,
|
|
11
13
|
refreshConnectKey,
|
|
12
14
|
resolveApiKey,
|
|
13
15
|
resolveBaseUrl,
|
|
14
16
|
sameInstance,
|
|
17
|
+
saveConfig as saveCoreConfig,
|
|
18
|
+
selectHubProfile,
|
|
15
19
|
selectProfile,
|
|
16
20
|
type CliIo as CoreCliIo,
|
|
17
21
|
type ConnectOnDemandOptions,
|
|
22
|
+
type ConnectOnDemandOutcome,
|
|
18
23
|
type ProfileSource
|
|
19
24
|
} from '@antasphere/cli-core';
|
|
20
25
|
import type { CliConfig, CliConfigStore, CliProfile } from './config.js';
|
|
21
|
-
import type
|
|
26
|
+
import { DEFAULT_HUB_URL, type CliIdentity } from './identity.js';
|
|
22
27
|
import {
|
|
23
28
|
isWorkspaceId,
|
|
24
29
|
matchWorkspace,
|
|
@@ -216,6 +221,15 @@ export interface CliContextKit<TClient extends ChassisClient<string>> {
|
|
|
216
221
|
profileUrl(profile: CliProfile | undefined): string;
|
|
217
222
|
/** The `connectOnDemand` options of one context (the tool, the instance, the profile, the wire). */
|
|
218
223
|
connectOptions(ctx: CliContext<TClient>): ConnectOnDemandOptions;
|
|
224
|
+
/**
|
|
225
|
+
* The hub exchange of one context: cli-core `connectOnDemand` with
|
|
226
|
+
* `connectOptions(ctx)` and `overrides`, and, after a fresh exchange, the
|
|
227
|
+
* hub profile healed with the address the hub answered when it has none.
|
|
228
|
+
*/
|
|
229
|
+
connect(
|
|
230
|
+
ctx: CliContext<TClient>,
|
|
231
|
+
overrides?: Partial<ConnectOnDemandOptions>
|
|
232
|
+
): Promise<ConnectOnDemandOutcome>;
|
|
219
233
|
}
|
|
220
234
|
|
|
221
235
|
/**
|
|
@@ -305,15 +319,18 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
|
|
|
305
319
|
? 'profile'
|
|
306
320
|
: 'none';
|
|
307
321
|
if (!apiKey && sameProfileInstance) {
|
|
308
|
-
// No direct key: the connect cache's slot is the
|
|
309
|
-
// (
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
322
|
+
// No direct key: the connect cache's slot is the hub profile of this
|
|
323
|
+
// run (the one ANTASPHERE_URL names, else the active one: what
|
|
324
|
+
// `antasphere login` stored), org-independent by design. A key is
|
|
325
|
+
// served only to the address that minted it (cli-core
|
|
326
|
+
// `cachedConnectKey`): another account's key is the other person's
|
|
327
|
+
// credential (PRDCT-3032), and a missing address on either side, the
|
|
328
|
+
// entry's or the hub profile's, is unknown, so nothing is served
|
|
329
|
+
// (PRDCT-3046); connectForKey retires the entry. This read repeats
|
|
330
|
+
// cli-core's own (connectOnDemand step 0) on purpose: the credential
|
|
331
|
+
// source and the recovering fetch are decided here.
|
|
332
|
+
const hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
|
|
333
|
+
if (hub.name) apiKey = cachedConnectKey(profile, hub.name, hub.profile?.email);
|
|
317
334
|
if (apiKey) credentialSource = 'hub-cache';
|
|
318
335
|
}
|
|
319
336
|
const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
|
|
@@ -344,21 +361,6 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
|
|
|
344
361
|
return ctx;
|
|
345
362
|
}
|
|
346
363
|
|
|
347
|
-
/**
|
|
348
|
-
* The cached connect key on `slot` was minted for another Antasphere
|
|
349
|
-
* account than the one whose login the slot now holds. Both addresses must
|
|
350
|
-
* be known: a cache entry or a hub profile without one is trusted as before.
|
|
351
|
-
*/
|
|
352
|
-
function cachedForAnotherAccount(
|
|
353
|
-
profile: CliProfile | undefined,
|
|
354
|
-
slot: string,
|
|
355
|
-
hubEmail: string | undefined
|
|
356
|
-
): boolean {
|
|
357
|
-
const cached = profile?.connectKeys?.[slot]?.email;
|
|
358
|
-
if (!cached || !hubEmail) return false;
|
|
359
|
-
return cached.toLowerCase() !== hubEmail.toLowerCase();
|
|
360
|
-
}
|
|
361
|
-
|
|
362
364
|
const NO_KEY_MESSAGE =
|
|
363
365
|
`An API key is required. Sign in (\`${identity.bin} login\`), pass --api-key, or set ` +
|
|
364
366
|
`${identity.envPrefix}_API_KEY.`;
|
|
@@ -376,6 +378,10 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
|
|
|
376
378
|
toolBaseUrl: ctx.baseUrl,
|
|
377
379
|
// A profile with no baseUrl of its own is at the cloud URL (cli-core 0.5.0).
|
|
378
380
|
cloudUrl: identity.cloudUrl,
|
|
381
|
+
// The same for a hub profile (cli-core 0.6.0, verifier round 1, F4):
|
|
382
|
+
// one with no baseUrl is at the default hub, so ANTASPHERE_URL naming
|
|
383
|
+
// that hub finds it, and the slot it names is the one this kit reads.
|
|
384
|
+
hubCloudUrl: DEFAULT_HUB_URL,
|
|
379
385
|
profileName: ctx.profileName ?? CLOUD_PROFILE,
|
|
380
386
|
env: io.env,
|
|
381
387
|
// Thread the injected fetch so the probe + exchange stay on the test
|
|
@@ -418,9 +424,14 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
|
|
|
418
424
|
}
|
|
419
425
|
const outcome = await refreshConnectKey(connectOptions(ctx));
|
|
420
426
|
if (outcome.outcome === 'not_cloud') return res;
|
|
427
|
+
// The cause before its consequence (verifier round 3, F1): the sign-in
|
|
428
|
+
// is said first, then what the fresh mint healed.
|
|
429
|
+
ctx.io.err.write('The cached key was refused; signed in again through Antasphere.\n');
|
|
430
|
+
// A fresh mint like any other: the hub profile is healed here too, or
|
|
431
|
+
// the next run would retire the key just minted (verifier round 2, F1).
|
|
432
|
+
healHubAddress(ctx, outcome);
|
|
421
433
|
ctx.apiKey = outcome.key;
|
|
422
434
|
ctx.credentialSource = 'hub-exchange';
|
|
423
|
-
ctx.io.err.write('The cached key was refused; signed in again through Antasphere.\n');
|
|
424
435
|
const headers = new Headers(init?.headers);
|
|
425
436
|
headers.set('authorization', `Bearer ${outcome.key}`);
|
|
426
437
|
return base(input, { ...init, headers });
|
|
@@ -515,32 +526,80 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
|
|
|
515
526
|
return ` (looked in the ${noun} "${selection.value}", selected by ${describeSelection(selection)})`;
|
|
516
527
|
}
|
|
517
528
|
|
|
529
|
+
async function connect(
|
|
530
|
+
ctx: CliContext<TClient>,
|
|
531
|
+
overrides: Partial<ConnectOnDemandOptions> = {}
|
|
532
|
+
): Promise<ConnectOnDemandOutcome> {
|
|
533
|
+
const outcome = await connectOnDemand({ ...connectOptions(ctx), ...overrides });
|
|
534
|
+
healHubAddress(ctx, outcome);
|
|
535
|
+
return outcome;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* The hub profile healed with the address the hub answered, after a fresh
|
|
540
|
+
* exchange: when it carries none or a blank one (an older login, a
|
|
541
|
+
* hand-edited file), or one the hub no longer answers (the account changed
|
|
542
|
+
* its address at the hub). Without it the slot would be refused and a key
|
|
543
|
+
* re-minted and revoked on every run, since a key is served only to the
|
|
544
|
+
* address that minted it. The answer is the key owner's own current
|
|
545
|
+
* address, so no other person's address can be written. That profile
|
|
546
|
+
* only, nothing else of the hub's file moves. A replaced address is said
|
|
547
|
+
* on stderr; an absent one is filled in silently. Called after every
|
|
548
|
+
* fresh exchange the kit runs: `connect` and the stranded-key recovery.
|
|
549
|
+
*/
|
|
550
|
+
function healHubAddress(ctx: CliContext<TClient>, outcome: ConnectOnDemandOutcome): void {
|
|
551
|
+
// `!outcome.cached`: a served key implies a hub address already equal to
|
|
552
|
+
// the entry's, so only a fresh exchange can have anything to heal; the
|
|
553
|
+
// guard states that intent.
|
|
554
|
+
if (outcome.outcome !== 'connected' || outcome.cached || !outcome.result) return;
|
|
555
|
+
// A chassis tool never answers a blank address; a foreign one must not
|
|
556
|
+
// blank the profile.
|
|
557
|
+
const answered = firstSet(outcome.result.user.email);
|
|
558
|
+
if (!answered) return;
|
|
559
|
+
const hub = selectHubProfile(ctx.io.env, { cloudUrl: DEFAULT_HUB_URL });
|
|
560
|
+
const hubConfig = loadCoreConfig(ctx.io.env, HUB_TOOL);
|
|
561
|
+
const stored = hub.name ? hubConfig.profiles[hub.name] : undefined;
|
|
562
|
+
const had = firstSet(stored?.email);
|
|
563
|
+
if (!hub.name || !stored || had?.toLowerCase() === answered.toLowerCase()) return;
|
|
564
|
+
hubConfig.profiles[hub.name] = { ...stored, email: answered };
|
|
565
|
+
saveCoreConfig(ctx.io.env, HUB_TOOL, hubConfig);
|
|
566
|
+
if (had) {
|
|
567
|
+
ctx.io.err.write(
|
|
568
|
+
`The Antasphere login's address is now ${answered}, replaced from ${had} (the hub's own answer).\n`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
518
573
|
async function connectForKey(ctx: CliContext<TClient>): Promise<string> {
|
|
519
574
|
const { io } = ctx;
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
// the
|
|
523
|
-
//
|
|
524
|
-
|
|
575
|
+
// An entry on the slot the cache refuses to serve (another account's
|
|
576
|
+
// key, the hub login on the slot having changed, PRDCT-3032; or a key
|
|
577
|
+
// whose address, or the hub profile's, is missing, PRDCT-3046) is
|
|
578
|
+
// revoked on its instance and forgotten before the exchange runs for the
|
|
579
|
+
// account now signed in, so the fresh mint never strands it live. Best
|
|
580
|
+
// effort: a dead key answers 401 and is forgotten all the same.
|
|
581
|
+
const hub = selectHubProfile(io.env, { cloudUrl: DEFAULT_HUB_URL });
|
|
525
582
|
if (ctx.profileName && hub.name) {
|
|
526
583
|
const profile = loadConfig(io.env).profiles[ctx.profileName];
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
584
|
+
const stale = profile?.connectKeys?.[hub.name];
|
|
585
|
+
if (stale && !cachedConnectKey(profile, hub.name, hub.profile?.email)) {
|
|
586
|
+
// A blank address is no address: it never reaches a sentence.
|
|
587
|
+
const hubEmail = firstSet(hub.profile?.email);
|
|
588
|
+
io.err.write(
|
|
589
|
+
`The ${identity.displayName} key cached for ${firstSet(stale.email) ?? 'an unknown login'} is retired: ` +
|
|
590
|
+
(hubEmail
|
|
591
|
+
? `the Antasphere login is now ${hubEmail}.\n`
|
|
592
|
+
: 'the Antasphere login carries no address to match it with.\n')
|
|
593
|
+
);
|
|
594
|
+
const auth = new CliAuthClient({
|
|
595
|
+
baseUrl: profileUrl(profile),
|
|
596
|
+
...(io.fetch ? { fetch: io.fetch } : {})
|
|
597
|
+
});
|
|
598
|
+
await auth.revoke(stale.apiKey).catch(() => undefined);
|
|
599
|
+
removeConnectKey(io.env, ctx.profileName, hub.name);
|
|
541
600
|
}
|
|
542
601
|
}
|
|
543
|
-
const outcome = await
|
|
602
|
+
const outcome = await connect(ctx);
|
|
544
603
|
if (outcome.outcome === 'not_cloud') {
|
|
545
604
|
// Self-hosted / unreachable: the classic error, byte-identical.
|
|
546
605
|
throw new CliUsageError(NO_KEY_MESSAGE);
|
|
@@ -566,7 +625,8 @@ export function createContext<TClient extends ChassisClient<string>>(input: {
|
|
|
566
625
|
explainWorkspaceRefusal,
|
|
567
626
|
workspaceNotFoundHint,
|
|
568
627
|
profileUrl,
|
|
569
|
-
connectOptions
|
|
628
|
+
connectOptions,
|
|
629
|
+
connect
|
|
570
630
|
};
|
|
571
631
|
}
|
|
572
632
|
|
package/src/identity.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { ToolIdentity } from '@antasphere/chassis-contract';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The Antasphere hub every tool signs in through, when nothing names another
|
|
5
|
+
* (`ANTASPHERE_URL`, a hub profile's own `baseUrl`): the instance of the
|
|
6
|
+
* implicit `cloud` hub profile, and the cloud URL the hub profile is selected
|
|
7
|
+
* with (cli-core `selectHubProfile`, `hubCloudUrl`), so that a hub profile
|
|
8
|
+
* with no `baseUrl` of its own is at this hub, as the account CLI reads it.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_HUB_URL = 'https://account.antasphere.com';
|
|
11
|
+
|
|
3
12
|
/**
|
|
4
13
|
* What a tool is called, in every place the generic CLI spells it. ONE object,
|
|
5
14
|
* passed by the tool to `defineCli` (index.ts): the chassis holds no product
|