@antasphere/chassis-cli 1.0.2 → 1.2.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/commands/webhooks.d.ts +36 -0
- package/dist/commands/webhooks.d.ts.map +1 -0
- package/dist/commands/webhooks.js +385 -0
- package/dist/commands/webhooks.js.map +1 -0
- 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 +13 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +9 -0
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +3 -0
- package/dist/program.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/auth.ts +39 -19
- package/src/commands/webhooks.ts +492 -0
- package/src/context.ts +109 -49
- package/src/identity.ts +15 -0
- package/src/index.ts +1 -0
- package/src/program.ts +3 -0
- package/test/host/identity.test.ts +3 -2
- package/test/host/minimal-tool.ts +2 -1
- 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/webhooks.test.ts +468 -0
- package/test/suite/whoami.test.ts +12 -2
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
|
|
@@ -21,6 +30,11 @@ export interface CliIdentity {
|
|
|
21
30
|
keyPrefix: string;
|
|
22
31
|
/** The scope a key needs for the full-workspace export, as `export --help` names it. */
|
|
23
32
|
exportScope: string;
|
|
33
|
+
/**
|
|
34
|
+
* The scope a key needs to manage webhooks (`identity.scopes.webhooks`), as
|
|
35
|
+
* the `webhooks` refusals name it; absent, the tool opens webhooks to no key.
|
|
36
|
+
*/
|
|
37
|
+
webhooksScope?: string | undefined;
|
|
24
38
|
/**
|
|
25
39
|
* The tool's cloud home (`identity.cloud.url`): the last step of the URL
|
|
26
40
|
* resolution and the instance of the implicit `cloud` profile, so a clean
|
|
@@ -43,6 +57,7 @@ export function cliIdentity(identity: ToolIdentity): CliIdentity {
|
|
|
43
57
|
envPrefix: identity.cli.envPrefix,
|
|
44
58
|
keyPrefix: identity.apiKeyPrefix,
|
|
45
59
|
exportScope: identity.scopes.dataExport,
|
|
60
|
+
webhooksScope: identity.scopes.webhooks,
|
|
46
61
|
cloudUrl: identity.cloud.url
|
|
47
62
|
};
|
|
48
63
|
}
|
package/src/index.ts
CHANGED
|
@@ -55,6 +55,7 @@ export { isInteractive, openInBrowser, platformOpener } from './open.js';
|
|
|
55
55
|
|
|
56
56
|
export { safeDownloadName } from './commands/files.js';
|
|
57
57
|
export { explainProjectRefusal, roleNamedBy, memberRef, type ProjectLookup } from './commands/projects.js';
|
|
58
|
+
export { explainWebhookRefusal, type WebhookLookup } from './commands/webhooks.js';
|
|
58
59
|
export { CliApiRefusal, drainPages } from './context.js';
|
|
59
60
|
export type { Cli, CliDefinition, CliKit, RegisterTool } from './kit.js';
|
|
60
61
|
|
package/src/program.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { registerAuthCommands } from './commands/auth.js';
|
|
|
8
8
|
import { registerWorkspaceCommands } from './commands/workspaces.js';
|
|
9
9
|
import { registerProjectCommands } from './commands/projects.js';
|
|
10
10
|
import { registerTeamCommands } from './commands/teams.js';
|
|
11
|
+
import { registerWebhookCommands } from './commands/webhooks.js';
|
|
11
12
|
import { registerDemoCommands } from './commands/demo.js';
|
|
12
13
|
import { registerFileCommands } from './commands/files.js';
|
|
13
14
|
import { registerCompletionCommand } from './commands/completion.js';
|
|
@@ -66,6 +67,8 @@ export function createProgram<TClient extends ChassisClient<string>>(
|
|
|
66
67
|
registerProjectCommands(kit, program, io);
|
|
67
68
|
// The named groups of the workspace's people: teams *.
|
|
68
69
|
registerTeamCommands(kit, program, io);
|
|
70
|
+
// The signed POSTs the workspace's events send to its URLs: webhooks *.
|
|
71
|
+
registerWebhookCommands(kit, program, io);
|
|
69
72
|
// Demo links, as a signed-in owner (never a key): demo link, list, revoke.
|
|
70
73
|
registerDemoCommands(kit, program, io);
|
|
71
74
|
// The tool's own groups, where they have always sat in `--help`.
|
|
@@ -190,10 +190,11 @@ const COMMANDS = [
|
|
|
190
190
|
'config',
|
|
191
191
|
'workspaces',
|
|
192
192
|
'workspace',
|
|
193
|
-
// generic: the subgroups of the workspace, its named groups of people,
|
|
194
|
-
// the owner's demo links
|
|
193
|
+
// generic: the subgroups of the workspace, its named groups of people, its
|
|
194
|
+
// webhooks, then the owner's demo links
|
|
195
195
|
'projects',
|
|
196
196
|
'teams',
|
|
197
|
+
'webhooks',
|
|
197
198
|
'demo',
|
|
198
199
|
// the tool's own group, at the seam
|
|
199
200
|
'things',
|
|
@@ -9,7 +9,7 @@ import type { Command } from 'commander';
|
|
|
9
9
|
* seam, so the tool's positional hook in the program is exercised on every
|
|
10
10
|
* run. It imports nothing from a tool.
|
|
11
11
|
*/
|
|
12
|
-
export type ThingsScope = 'things:read' | 'things:write' | 'things:export';
|
|
12
|
+
export type ThingsScope = 'things:read' | 'things:write' | 'things:export' | 'things:webhooks';
|
|
13
13
|
|
|
14
14
|
export const VERSION = '0.0.1';
|
|
15
15
|
|
|
@@ -22,6 +22,7 @@ export const cli = defineCli({
|
|
|
22
22
|
envPrefix: 'THINGS',
|
|
23
23
|
keyPrefix: 'thk',
|
|
24
24
|
exportScope: 'things:export',
|
|
25
|
+
webhooksScope: 'things:webhooks',
|
|
25
26
|
cloudUrl: 'https://things.antasphere.test'
|
|
26
27
|
},
|
|
27
28
|
description: 'Command-line client for a Things instance',
|