@antasphere/chassis-cli 0.0.0-stage → 1.0.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.
Files changed (117) hide show
  1. package/LICENSE +82 -0
  2. package/dist/commands/auth.d.ts +6 -0
  3. package/dist/commands/auth.d.ts.map +1 -0
  4. package/dist/commands/auth.js +582 -0
  5. package/dist/commands/auth.js.map +1 -0
  6. package/dist/commands/completion.d.ts +5 -0
  7. package/dist/commands/completion.d.ts.map +1 -0
  8. package/dist/commands/completion.js +85 -0
  9. package/dist/commands/completion.js.map +1 -0
  10. package/dist/commands/demo.d.ts +12 -0
  11. package/dist/commands/demo.d.ts.map +1 -0
  12. package/dist/commands/demo.js +210 -0
  13. package/dist/commands/demo.js.map +1 -0
  14. package/dist/commands/files.d.ts +21 -0
  15. package/dist/commands/files.d.ts.map +1 -0
  16. package/dist/commands/files.js +178 -0
  17. package/dist/commands/files.js.map +1 -0
  18. package/dist/commands/projects.d.ts +38 -0
  19. package/dist/commands/projects.d.ts.map +1 -0
  20. package/dist/commands/projects.js +386 -0
  21. package/dist/commands/projects.js.map +1 -0
  22. package/dist/commands/teams.d.ts +46 -0
  23. package/dist/commands/teams.d.ts.map +1 -0
  24. package/dist/commands/teams.js +265 -0
  25. package/dist/commands/teams.js.map +1 -0
  26. package/dist/commands/workspaces.d.ts +22 -0
  27. package/dist/commands/workspaces.d.ts.map +1 -0
  28. package/dist/commands/workspaces.js +116 -0
  29. package/dist/commands/workspaces.js.map +1 -0
  30. package/dist/config.d.ts +29 -0
  31. package/dist/config.d.ts.map +1 -0
  32. package/dist/config.js +54 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/context.d.ts +152 -0
  35. package/dist/context.d.ts.map +1 -0
  36. package/dist/context.js +456 -0
  37. package/dist/context.js.map +1 -0
  38. package/dist/identity.d.ts +36 -0
  39. package/dist/identity.d.ts.map +1 -0
  40. package/dist/identity.js +18 -0
  41. package/dist/identity.js.map +1 -0
  42. package/dist/index.d.ts +24 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +31 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/kit.d.ts +46 -0
  47. package/dist/kit.d.ts.map +1 -0
  48. package/dist/kit.js +2 -0
  49. package/dist/kit.js.map +1 -0
  50. package/dist/open.d.ts +27 -0
  51. package/dist/open.d.ts.map +1 -0
  52. package/dist/open.js +45 -0
  53. package/dist/open.js.map +1 -0
  54. package/dist/owner-session.d.ts +34 -0
  55. package/dist/owner-session.d.ts.map +1 -0
  56. package/dist/owner-session.js +106 -0
  57. package/dist/owner-session.js.map +1 -0
  58. package/dist/program.d.ts +5 -0
  59. package/dist/program.d.ts.map +1 -0
  60. package/dist/program.js +150 -0
  61. package/dist/program.js.map +1 -0
  62. package/dist/safe-write.d.ts +46 -0
  63. package/dist/safe-write.d.ts.map +1 -0
  64. package/dist/safe-write.js +127 -0
  65. package/dist/safe-write.js.map +1 -0
  66. package/dist/stdin.d.ts +4 -0
  67. package/dist/stdin.d.ts.map +1 -0
  68. package/dist/stdin.js +36 -0
  69. package/dist/stdin.js.map +1 -0
  70. package/dist/testing/index.d.ts +70 -0
  71. package/dist/testing/index.d.ts.map +1 -0
  72. package/dist/testing/index.js +67 -0
  73. package/dist/testing/index.js.map +1 -0
  74. package/dist/workspace.d.ts +79 -0
  75. package/dist/workspace.d.ts.map +1 -0
  76. package/dist/workspace.js +139 -0
  77. package/dist/workspace.js.map +1 -0
  78. package/package.json +43 -3
  79. package/src/commands/auth.ts +687 -0
  80. package/src/commands/completion.ts +102 -0
  81. package/src/commands/demo.ts +271 -0
  82. package/src/commands/files.ts +197 -0
  83. package/src/commands/projects.ts +460 -0
  84. package/src/commands/teams.ts +320 -0
  85. package/src/commands/workspaces.ts +140 -0
  86. package/src/config.ts +81 -0
  87. package/src/context.ts +616 -0
  88. package/src/identity.ts +48 -0
  89. package/src/index.ts +70 -0
  90. package/src/kit.ts +50 -0
  91. package/src/open.ts +48 -0
  92. package/src/owner-session.ts +152 -0
  93. package/src/program.ts +183 -0
  94. package/src/safe-write.ts +136 -0
  95. package/src/stdin.ts +39 -0
  96. package/src/testing/index.ts +129 -0
  97. package/src/workspace.ts +229 -0
  98. package/test/host/identity.test.ts +229 -0
  99. package/test/host/minimal-host.ts +16 -0
  100. package/test/host/minimal-tool.ts +40 -0
  101. package/test/suite/cli.test.ts +301 -0
  102. package/test/suite/config.test.ts +59 -0
  103. package/test/suite/demo.test.ts +467 -0
  104. package/test/suite/hub-profile-shared.test.ts +123 -0
  105. package/test/suite/login.test.ts +743 -0
  106. package/test/suite/logout.test.ts +247 -0
  107. package/test/suite/migration.test.ts +109 -0
  108. package/test/suite/open.test.ts +59 -0
  109. package/test/suite/profiles.test.ts +380 -0
  110. package/test/suite/projects.test.ts +550 -0
  111. package/test/suite/recovery.test.ts +348 -0
  112. package/test/suite/security.test.ts +199 -0
  113. package/test/suite/signin-fixtures.ts +204 -0
  114. package/test/suite/teams.test.ts +354 -0
  115. package/test/suite/whoami.test.ts +159 -0
  116. package/test/suite/workspace.test.ts +402 -0
  117. package/README.md +0 -3
package/LICENSE ADDED
@@ -0,0 +1,82 @@
1
+ # License
2
+
3
+ The Antasphere chassis
4
+ Copyright (c) 2026 Antasphere
5
+
6
+ The Antasphere chassis is made available by Antasphere, the licensor, under the "Sustainable Use License" as
7
+ defined below. All third party components incorporated into the Antasphere chassis are licensed under the
8
+ original license provided by the owner of the applicable component.
9
+
10
+ ## Sustainable Use License
11
+
12
+ Version 1.0
13
+
14
+ ### Acceptance
15
+
16
+ By using the software, you agree to all of the terms and conditions below.
17
+
18
+ ### Copyright License
19
+
20
+ The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
21
+ to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
22
+ to the limitations below.
23
+
24
+ ### Limitations
25
+
26
+ You may use or modify the software only for your own internal business purposes or for non-commercial or
27
+ personal use. You may distribute the software or provide it to others only if you do so free of charge for
28
+ non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
29
+ the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
30
+
31
+ ### Patents
32
+
33
+ The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
34
+ license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
35
+ subject to the limitations and conditions in this license. This license does not cover any patent claims that
36
+ you cause to be infringed by modifications or additions to the software. If you or your company make any
37
+ written claim that the software infringes or contributes to infringement of any patent, your patent license
38
+ for the software granted under these terms ends immediately. If your company makes such a claim, your patent
39
+ license ends immediately for work on behalf of your company.
40
+
41
+ ### Notices
42
+
43
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
44
+ terms. If you modify the software, you must include in any modified copies of the software a prominent notice
45
+ stating that you have modified the software.
46
+
47
+ ### No Other Rights
48
+
49
+ These terms do not imply any licenses other than those expressly granted in these terms.
50
+
51
+ ### Termination
52
+
53
+ If you use the software in violation of these terms, such use is not licensed, and your license will
54
+ automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
55
+ violation of this license no later than 30 days after you receive that notice, your license will be reinstated
56
+ retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
57
+ terms will cause your license to terminate automatically and permanently.
58
+
59
+ ### No Liability
60
+
61
+ As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
62
+ not be liable to you for any damages arising out of these terms or the use or nature of the software, under
63
+ any kind of legal claim.
64
+
65
+ ### Definitions
66
+
67
+ The “licensor” is the entity offering these terms.
68
+
69
+ The “software” is the software the licensor makes available under these terms, including any portion of it.
70
+
71
+ “You” refers to the individual or entity agreeing to these terms.
72
+
73
+ “Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
74
+ all organizations that have control over, are under the control of, or are under common control with that
75
+ organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
76
+ management and policies by vote, contract, or otherwise. Control can be direct or indirect.
77
+
78
+ “Your license” is the license granted to you for the software under these terms.
79
+
80
+ “Use” means anything you do with the software requiring your license.
81
+
82
+ “Trademark” means trademarks, service marks, and similar rights.
@@ -0,0 +1,6 @@
1
+ import type { Command } from 'commander';
2
+ import type { ChassisClient } from '@antasphere/chassis-sdk';
3
+ import { type CliIo } from '../context.js';
4
+ import type { CliKit } from '../kit.js';
5
+ export declare function registerAuthCommands<TClient extends ChassisClient<string>>(kit: CliKit<TClient>, program: Command, io: CliIo): void;
6
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAML,KAAK,KAAK,EAEX,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAuExC,wBAAgB,oBAAoB,CAAC,OAAO,SAAS,aAAa,CAAC,MAAM,CAAC,EACxE,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,EACpB,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,KAAK,GACR,IAAI,CAqkBN"}
@@ -0,0 +1,582 @@
1
+ import { activeHubProfile, ask, CliAuthClient, CliAuthError, CliUsageError as CoreUsageError, CLOUD_PROFILE, connectOnDemand, HUB_TOOL, loadConfig as loadCoreConfig, normalizeUrl, probeToolInstance, resolveBaseUrl, sameInstance, selectProfile, saveConfig as saveCoreConfig } from '@antasphere/cli-core';
2
+ import { redactKey } from '../config.js';
3
+ import { CliUsageError, printJson, stdinApiKey, table } from '../context.js';
4
+ /**
5
+ * Identity + profile commands (PRDCT-2947, PRDCT-2446): ONE `login` that
6
+ * ends in the tool, `logout` that revokes what it forgets, whoami/verify,
7
+ * and profile management (use / profiles / config show / config clear).
8
+ *
9
+ * A profile is an instance (cli-core 0.5.0): `login` with no URL signs in
10
+ * on the profile the run selects (the active one, else the implicit `cloud`
11
+ * profile at the tool's cloud URL); `--api-url <url>` selects or creates the
12
+ * profile of that instance, named for its host. On an Antasphere-cloud
13
+ * instance the login is the hub's email code (once, stored on the hub
14
+ * profile exactly as `antasphere login` stores it), then the exchange that
15
+ * mints the tool's own key; on a self-hosted instance it is the instance's
16
+ * email code, or a pasted key.
17
+ */
18
+ /** The hub a tool CLI signs in against inline: the same variable the hub CLI reads. */
19
+ const HUB_URL_ENV = 'ANTASPHERE_URL';
20
+ const DEFAULT_HUB_URL = 'https://account.antasphere.com';
21
+ /** How a signed-in credential reads in a sentence. */
22
+ function describeCredential(source, profileName) {
23
+ switch (source) {
24
+ case 'flag':
25
+ return 'the --api-key flag';
26
+ case 'env':
27
+ return 'the API key variable';
28
+ case 'profile':
29
+ return profileName ? `the key of profile "${profileName}"` : 'the profile key';
30
+ case 'hub-cache':
31
+ return 'the Antasphere login (cached key)';
32
+ case 'hub-exchange':
33
+ return 'the Antasphere login (fresh exchange)';
34
+ case 'none':
35
+ return 'nothing';
36
+ }
37
+ }
38
+ /** `Signed in as Ada <ada@x.co> in Acme (organization …) on <url>`. */
39
+ function signedInLine(me, baseUrl) {
40
+ const who = me.user.name ? `${me.user.name} <${me.user.email}>` : me.user.email;
41
+ const active = me.workspace ? me.workspaces.find((w) => w.id === me.workspace.id) : undefined;
42
+ const org = active && active.centralAccountId;
43
+ const where = me.workspace
44
+ ? ` in ${me.workspace.name}${org ? ` (organization ${org})` : ''}`
45
+ : ' with no workspace yet';
46
+ return `Signed in as ${who}${where} on ${baseUrl}.`;
47
+ }
48
+ /** The one line listing the other organizations, with the --org hint, or ''. */
49
+ function organizationsLine(me) {
50
+ if (me.workspaces.length < 2)
51
+ return '';
52
+ const names = me.workspaces.map((w) => w.name).join(', ');
53
+ return `Your workspaces: ${names}. Pass --org <name> (or --workspace <name>) to work in another.\n`;
54
+ }
55
+ export function registerAuthCommands(kit, program, io) {
56
+ const { identity, createClient, clearConfig, configPath, connectOptions, loadConfig, profileUrl, removeConnectKey, requireApiKey, resolveContext, saveConfig, workspaceSource } = kit;
57
+ const { describeSource } = kit.workspace;
58
+ /** Save the tool's own key on a profile and make that profile the active one. */
59
+ function saveProfileKey(profileName, baseUrl, apiKey, email) {
60
+ const config = loadConfig(io.env);
61
+ const previous = config.profiles[profileName];
62
+ config.profiles[profileName] = {
63
+ ...previous,
64
+ apiKey,
65
+ baseUrl: normalizeUrl(baseUrl),
66
+ ...(email ? { email } : {})
67
+ };
68
+ config.activeProfile = profileName;
69
+ return saveConfig(io.env, config);
70
+ }
71
+ /** `login` makes its profile the active one: a login is the choice of where the next command runs. */
72
+ function activate(profileName, baseUrl) {
73
+ const config = loadConfig(io.env);
74
+ config.profiles[profileName] = { ...config.profiles[profileName], baseUrl: normalizeUrl(baseUrl) };
75
+ config.activeProfile = profileName;
76
+ return saveConfig(io.env, config);
77
+ }
78
+ /**
79
+ * The hub's email-code sign-in, inline: the wire `antasphere login` speaks
80
+ * (cli-core's instance auth client against the hub), the key written on the
81
+ * hub profile exactly as that CLI writes it, so the two logins are one.
82
+ */
83
+ async function hubLogin(opts) {
84
+ // The hub profile the hub CLI would use, resolved its way (verifier
85
+ // round 1, F4): ANTASPHERE_URL selects the profile of that hub, else the
86
+ // active one, else the implicit `cloud` at account.antasphere.com; the
87
+ // URL is the variable, else that profile's own, else the default. A
88
+ // stored hub URL is never repointed at the production hub.
89
+ const hubConfig = loadCoreConfig(io.env, HUB_TOOL);
90
+ const selected = selectProfile({
91
+ config: hubConfig,
92
+ apiUrl: io.env[HUB_URL_ENV],
93
+ cloudUrl: DEFAULT_HUB_URL
94
+ });
95
+ const profile = selected.name ?? CLOUD_PROFILE;
96
+ const hubUrl = resolveBaseUrl({
97
+ env: io.env,
98
+ envVar: HUB_URL_ENV,
99
+ profile: selected.profile,
100
+ cloudUrl: DEFAULT_HUB_URL
101
+ });
102
+ const auth = new CliAuthClient({ baseUrl: hubUrl, ...(io.fetch ? { fetch: io.fetch } : {}) });
103
+ const email = opts.email ?? (await ask(io, 'Antasphere email: '));
104
+ await auth.request({ email });
105
+ io.err.write(`A sign-in code was sent to ${email} (if that Antasphere account can receive mail).\n`);
106
+ const otp = await ask(io, 'Code: ');
107
+ const completed = await auth.complete({
108
+ email,
109
+ otp,
110
+ ...(opts.keyName ? { keyName: opts.keyName } : {}),
111
+ ...(opts.expiresInDays ? { expiresInDays: opts.expiresInDays } : {})
112
+ });
113
+ // Only the key's own fields move, exactly as `antasphere login` writes
114
+ // them; whatever else the profile holds stays.
115
+ const fresh = loadCoreConfig(io.env, HUB_TOOL);
116
+ fresh.profiles[profile] = {
117
+ ...fresh.profiles[profile],
118
+ apiKey: completed.key,
119
+ baseUrl: hubUrl,
120
+ email: completed.user.email,
121
+ workspaceId: completed.workspaceId
122
+ };
123
+ fresh.activeProfile = profile;
124
+ saveCoreConfig(io.env, HUB_TOOL, fresh);
125
+ return { email: completed.user.email, profile };
126
+ }
127
+ /** The instance's own email-code sign-in (self-hosted): both legs in one command. */
128
+ async function instanceLogin(baseUrl, opts) {
129
+ const auth = new CliAuthClient({ baseUrl, ...(io.fetch ? { fetch: io.fetch } : {}) });
130
+ const email = opts.email ?? (await ask(io, 'Email: '));
131
+ await auth.request({ email });
132
+ io.err.write(`A sign-in code was sent to ${email} (if that account exists on ${baseUrl}).\n`);
133
+ const otp = await ask(io, 'Code: ');
134
+ const completed = await auth.complete({
135
+ email,
136
+ otp,
137
+ ...(opts.keyName ? { keyName: opts.keyName } : {}),
138
+ ...(opts.expiresInDays ? { expiresInDays: opts.expiresInDays } : {})
139
+ });
140
+ return { key: completed.key, keyName: completed.apiKey.name, email: completed.user.email };
141
+ }
142
+ program
143
+ .command('login')
144
+ .description(`Sign in: on the cloud through your Antasphere account, on a self-hosted instance with an email code ` +
145
+ `or a pasted ${identity.keyPrefix}_ key; saves the profile and makes it the active one`)
146
+ .option('--email <email>', 'the account email (prompted when omitted)')
147
+ .option('--key-name <name>', 'display name for the key the sign-in mints')
148
+ .option('--expires-in-days <n>', 'TTL in days of that key (default: never expires)', (v) => parseInt(v, 10))
149
+ .action(async (opts, cmd) => {
150
+ const globals = cmd.optsWithGlobals();
151
+ const ctx = resolveContext(cmd, io);
152
+ const profileName = ctx.profileName ?? CLOUD_PROFILE;
153
+ const { baseUrl } = ctx;
154
+ // A profile is an instance, and a login never repoints one (verifier
155
+ // round 2, F9): a saved profile named with --profile while --api-url
156
+ // (or the URL variable) names another instance is refused before any
157
+ // request, so no key of that profile ever travels to the other origin.
158
+ const saved = ctx.config.profiles[profileName];
159
+ if (saved && !sameInstance(profileUrl(saved), baseUrl)) {
160
+ throw new CliUsageError(`Profile "${profileName}" is the instance ${profileUrl(saved)}, and this login targets ${baseUrl}. ` +
161
+ `Drop --profile to sign in on the profile of ${baseUrl}, or drop --api-url to sign in on "${profileName}".`);
162
+ }
163
+ // A pasted key: `--api-key`, `--api-key-stdin` (already spent in
164
+ // index.ts; reuse that read), or the key variable. Verified before it
165
+ // is stored. A profile key is NOT a paste: `login` on a profile that
166
+ // already holds one re-runs the sign-in the person asked for.
167
+ // A paste is an act: `--api-key` or `--api-key-stdin`, never the key
168
+ // variable a shell or a CI job happens to carry (verifier round 1, F8).
169
+ const pasted = globals.apiKey ?? stdinApiKey(io);
170
+ if (pasted !== undefined) {
171
+ if (!pasted.startsWith(`${identity.keyPrefix}_`)) {
172
+ throw new CliUsageError(`That is not a ${identity.displayName} key: it should start with ${identity.keyPrefix}_.`);
173
+ }
174
+ const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
175
+ const me = await createClient({ baseUrl, apiKey: pasted, fetch: fetchImpl }).me();
176
+ const path = saveProfileKey(profileName, baseUrl, pasted, me.user.email);
177
+ if (globals.json) {
178
+ return printJson(io, {
179
+ profile: profileName,
180
+ baseUrl,
181
+ via: 'key',
182
+ user: me.user,
183
+ workspace: me.workspace,
184
+ configPath: path
185
+ });
186
+ }
187
+ io.out.write(`${signedInLine(me, baseUrl)}\nKey saved to profile "${profileName}" (${path}).\n${organizationsLine(me)}`);
188
+ return;
189
+ }
190
+ // Cloud or self-hosted? Discovery says (the same probe as connect-on-demand).
191
+ const probe = await probeToolInstance(baseUrl, io.fetch);
192
+ if (probe?.cloud) {
193
+ // The hub login, once: stored on the hub profile as `antasphere login`
194
+ // stores it, so every other tool CLI is signed in too. `--email`
195
+ // naming ANOTHER account than the stored login signs that account in
196
+ // afresh, replacing the stored login (PRDCT-3032): the person who
197
+ // signed in with the wrong address switches from here.
198
+ let hub = activeHubProfile(io.env);
199
+ const stored = hub.profile?.apiKey ? hub.profile.email : undefined;
200
+ const switching = Boolean(stored) && opts.email !== undefined && opts.email.toLowerCase() !== stored.toLowerCase();
201
+ if (switching) {
202
+ io.err.write(`Replacing the Antasphere login ${stored} with ${opts.email}.\n`);
203
+ }
204
+ if (!hub.profile?.apiKey || switching)
205
+ await hubLogin(opts);
206
+ // A login is an explicit re-sign-in (verifier round 1, F3): the key
207
+ // cached on this profile is revoked on its instance (best effort: a
208
+ // dead one answers 401) and forgotten, and the exchange runs afresh,
209
+ // so a stale cached key never survives a login.
210
+ hub = activeHubProfile(io.env);
211
+ const stale = hub.name
212
+ ? loadConfig(io.env).profiles[profileName]?.connectKeys?.[hub.name]
213
+ : undefined;
214
+ if (stale && hub.name) {
215
+ // Revoked on the instance the key was minted for, the profile's own
216
+ // (the same rule as logout), never on a flag URL.
217
+ const auth = new CliAuthClient({
218
+ baseUrl: profileUrl(loadConfig(io.env).profiles[profileName]),
219
+ ...(io.fetch ? { fetch: io.fetch } : {})
220
+ });
221
+ await auth.revoke(stale.apiKey).catch(() => undefined);
222
+ removeConnectKey(io.env, profileName, hub.name);
223
+ }
224
+ // The exchange, then the tool's own answer. A stored hub key the hub
225
+ // no longer accepts is replaced by a fresh sign-in, once.
226
+ let outcome;
227
+ try {
228
+ outcome = await connectOnDemand({ ...connectOptions(ctx), profileName, cached: false });
229
+ }
230
+ catch (e) {
231
+ if (!(e instanceof CoreUsageError) || !/rejected/.test(e.message))
232
+ throw e;
233
+ io.err.write('The stored Antasphere login was rejected: signing in again.\n');
234
+ await hubLogin(opts);
235
+ hub = activeHubProfile(io.env);
236
+ outcome = await connectOnDemand({ ...connectOptions(ctx), profileName, cached: false });
237
+ }
238
+ if (outcome.outcome === 'not_cloud') {
239
+ throw new CliUsageError(`${baseUrl} stopped answering as an Antasphere-cloud instance; try again.`);
240
+ }
241
+ const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
242
+ const me = await createClient({ baseUrl, apiKey: outcome.key, fetch: fetchImpl }).me();
243
+ const path = activate(profileName, baseUrl);
244
+ if (globals.json) {
245
+ return printJson(io, {
246
+ profile: profileName,
247
+ baseUrl,
248
+ via: 'antasphere',
249
+ hubProfile: hub.name ?? CLOUD_PROFILE,
250
+ user: me.user,
251
+ workspace: me.workspace,
252
+ workspaces: me.workspaces,
253
+ configPath: path
254
+ });
255
+ }
256
+ io.out.write(`${signedInLine(me, baseUrl)}\n${organizationsLine(me)}`);
257
+ return;
258
+ }
259
+ // Self-hosted: the instance's own email code, then its key on the profile.
260
+ const minted = await instanceLogin(baseUrl, opts);
261
+ const fetchImpl = io.fetch ?? globalThis.fetch.bind(globalThis);
262
+ const me = await createClient({ baseUrl, apiKey: minted.key, fetch: fetchImpl }).me();
263
+ const path = saveProfileKey(profileName, baseUrl, minted.key, minted.email);
264
+ if (globals.json) {
265
+ // The full key is deliberately NOT echoed — it is already stored.
266
+ return printJson(io, {
267
+ profile: profileName,
268
+ baseUrl,
269
+ via: 'otp',
270
+ user: me.user,
271
+ workspace: me.workspace,
272
+ keyName: minted.keyName,
273
+ configPath: path
274
+ });
275
+ }
276
+ io.out.write(`${signedInLine(me, baseUrl)}\nAPI key "${minted.keyName}" (${redactKey(minted.key)}) saved to profile ` +
277
+ `"${profileName}" (${path}).\n${organizationsLine(me)}`);
278
+ });
279
+ /** The hub login `--all` ends: `antasphere logout` from here (the key revoked on the hub, the profile cleared). */
280
+ async function hubLogout() {
281
+ const hub = activeHubProfile(io.env);
282
+ if (!hub.name || !hub.profile?.apiKey)
283
+ return null;
284
+ const auth = new CliAuthClient({
285
+ baseUrl: profileUrl(hub.profile),
286
+ ...(io.fetch ? { fetch: io.fetch } : {})
287
+ });
288
+ let revoked = false;
289
+ try {
290
+ await auth.revoke(hub.profile.apiKey);
291
+ revoked = true;
292
+ }
293
+ catch (e) {
294
+ if (!(e instanceof CliAuthError && (e.status === 401 || e.status === 403)))
295
+ throw e;
296
+ io.err.write('The stored Antasphere login was already unusable; forgetting it.\n');
297
+ }
298
+ const fresh = loadCoreConfig(io.env, HUB_TOOL);
299
+ delete fresh.profiles[hub.name];
300
+ if (fresh.activeProfile === hub.name)
301
+ delete fresh.activeProfile;
302
+ saveCoreConfig(io.env, HUB_TOOL, fresh);
303
+ return { email: hub.profile.email, revoked };
304
+ }
305
+ program
306
+ .command('logout')
307
+ .description('Sign out of the active (or named) profile: revokes every key it holds on the instance, then forgets them; ' +
308
+ 'with --all, also ends the Antasphere login of this machine (what `antasphere logout` does)')
309
+ .option('--all', 'also revoke the Antasphere login and clear its profile', false)
310
+ .action(async (opts, cmd) => {
311
+ const globals = cmd.optsWithGlobals();
312
+ const ctx = resolveContext(cmd, io);
313
+ const profileName = ctx.profileName;
314
+ const profile = profileName ? ctx.config.profiles[profileName] : undefined;
315
+ const hubLogin = activeHubProfile(io.env).profile;
316
+ if (!profileName || !profile) {
317
+ // Nothing of this tool's on the machine. The Antasphere login, when
318
+ // there is one, is the family's: it is ended here on --all, else
319
+ // named with the two commands that end it (PRDCT-3032).
320
+ if (hubLogin?.apiKey) {
321
+ if (opts.all) {
322
+ const ended = await hubLogout();
323
+ if (globals.json)
324
+ return printJson(io, { profile: null, revoked: 0, refused: 0, hub: ended });
325
+ io.out.write(`Nothing of ${identity.displayName}'s to sign out of on this machine. ` +
326
+ `The Antasphere login ${ended?.email ?? ''} is ${ended?.revoked ? 'revoked and ' : ''}forgotten.\n`);
327
+ return;
328
+ }
329
+ throw new CliUsageError(`Nothing of ${identity.displayName}'s to sign out of on this machine. The Antasphere login ` +
330
+ `${hubLogin.email ?? ''} stays for the whole tool family: \`${identity.bin} logout --all\` or ` +
331
+ '`antasphere logout` ends it.');
332
+ }
333
+ throw new CliUsageError('No profile to log out of.');
334
+ }
335
+ // Every key is revoked on the instance the PROFILE names, never on a
336
+ // flag/env URL: a key must not travel to a different instance, not
337
+ // even to die (a foreign host would see the key AND the real one would
338
+ // survive while we report it gone).
339
+ const baseUrl = profileUrl(profile);
340
+ const auth = new CliAuthClient({ baseUrl, ...(io.fetch ? { fetch: io.fetch } : {}) });
341
+ let revoked = 0;
342
+ let refused = 0;
343
+ const revoke = async (key, what) => {
344
+ try {
345
+ await auth.revoke(key);
346
+ revoked += 1;
347
+ }
348
+ catch (e) {
349
+ if (e instanceof CliAuthError && e.status === 401) {
350
+ io.err.write(`${what} was already unusable; forgetting it.\n`);
351
+ }
352
+ else if (e instanceof CliAuthError && e.status === 403) {
353
+ // The key WORKS but the instance refused the self-revoke (an
354
+ // older instance whose machine allowlist predates DELETE
355
+ // /cli/auth/key). Never report that as gone.
356
+ refused += 1;
357
+ io.err.write(`The instance refused to revoke ${what} (${e.code}): it STAYS VALID server-side; revoke it from ` +
358
+ 'the dashboard. Forgetting the local copy.\n');
359
+ }
360
+ else {
361
+ throw e;
362
+ }
363
+ }
364
+ };
365
+ const slots = Object.entries(profile.connectKeys ?? {});
366
+ for (const [slot, entry] of slots) {
367
+ await revoke(entry.apiKey, `the key of the Antasphere login "${slot}"`);
368
+ removeConnectKey(io.env, profileName, slot);
369
+ }
370
+ if (profile.apiKey)
371
+ await revoke(profile.apiKey, 'the profile key');
372
+ const config = loadConfig(io.env);
373
+ const current = config.profiles[profileName];
374
+ if (current) {
375
+ delete current.apiKey;
376
+ delete current.email;
377
+ saveConfig(io.env, config);
378
+ }
379
+ const ended = opts.all ? await hubLogout() : null;
380
+ if (globals.json) {
381
+ return printJson(io, {
382
+ profile: profileName,
383
+ baseUrl,
384
+ revoked,
385
+ refused,
386
+ hubProfiles: slots.map(([slot]) => slot),
387
+ forgotten: true,
388
+ ...(opts.all ? { hub: ended } : {})
389
+ });
390
+ }
391
+ const hubLine = opts.all
392
+ ? ended
393
+ ? ` The Antasphere login ${ended.email ?? ''} is ${ended.revoked ? 'revoked and ' : ''}forgotten.`
394
+ : ' No Antasphere login to end.'
395
+ : hubLogin?.apiKey
396
+ ? ` The Antasphere login ${hubLogin.email ?? ''} stays for the whole tool family (\`${identity.bin} logout --all\` ends it too).`
397
+ : '';
398
+ if (revoked === 0 && slots.length === 0 && !profile.apiKey) {
399
+ io.out.write(`Profile "${profileName}" held no key.${hubLine}\n`);
400
+ return;
401
+ }
402
+ io.out.write(`Logged out of profile "${profileName}" on ${baseUrl}` +
403
+ (revoked > 0 ? ` (${revoked} key${revoked === 1 ? '' : 's'} revoked server-side)` : '') +
404
+ (refused > 0
405
+ ? ` — ${refused} key${refused === 1 ? '' : 's'} could NOT be revoked and stay valid`
406
+ : '') +
407
+ `.${hubLine}\n`);
408
+ });
409
+ program
410
+ .command('whoami')
411
+ .description('Show the identity behind the resolved API key, and where it came from')
412
+ .action(async (_opts, cmd) => {
413
+ const ctx = resolveContext(cmd, io);
414
+ await requireApiKey(ctx);
415
+ const me = await ctx.client.me();
416
+ // `me` is the answer WITH the selection applied, so its workspace is
417
+ // the one this command (and any other, same flags) really ran in.
418
+ const source = workspaceSource(ctx);
419
+ const hub = activeHubProfile(io.env);
420
+ const hubLine = hubLoginLine(ctx, hub.profile);
421
+ if (ctx.json) {
422
+ return printJson(io, {
423
+ ...me,
424
+ workspaceSource: source,
425
+ credentialSource: ctx.credentialSource,
426
+ profile: ctx.profileName ?? null,
427
+ hubLogin: hub.profile?.apiKey ? { profile: hub.name, email: hub.profile.email ?? null } : null
428
+ });
429
+ }
430
+ const active = me.workspace ? me.workspaces.find((w) => w.id === me.workspace.id) : undefined;
431
+ const org = active && active.centralAccountId;
432
+ io.out.write(`${me.user.name} <${me.user.email}>\n` +
433
+ ` instance: ${ctx.baseUrl} (profile "${ctx.profileName ?? '-'}")\n` +
434
+ // A key is a USER credential: with zero memberships /me can carry
435
+ // no active workspace (the CLI only ever sees this for sessions,
436
+ // but the wire shape is honest about it).
437
+ ` workspace: ${me.workspace ? `${me.workspace.name} (${me.workspace.id})` : '(none)'}\n` +
438
+ (org ? ` organization: ${org}\n` : '') +
439
+ ` chosen by: ${describeSource(source, ctx.workspaceSelection?.kind)}\n` +
440
+ ` signed in with: ${describeCredential(ctx.credentialSource, ctx.profileName)}\n` +
441
+ (hubLine ? ` Antasphere login: ${hubLine}\n` : '') +
442
+ ` role: ${me.role ?? '(none)'} (via ${me.via})\n` +
443
+ ` scopes: ${me.scopes ? me.scopes.join(', ') : 'full (session)'}\n` +
444
+ (me.via === 'api_key' ? ` key expires: ${me.apiKeyExpiresAt ?? 'never'}\n` : ''));
445
+ });
446
+ /**
447
+ * `whoami`'s line about the hub login: which one served, or why the stored
448
+ * one was NOT used (a direct key won, the profile names another instance),
449
+ * or that none exists where one would serve (the cloud profile).
450
+ */
451
+ function hubLoginLine(ctx, hub) {
452
+ const stored = hub?.apiKey ? (hub.email ?? 'stored') : null;
453
+ if (ctx.credentialSource === 'hub-cache' || ctx.credentialSource === 'hub-exchange') {
454
+ return stored ?? 'used';
455
+ }
456
+ if (stored) {
457
+ const reason = ctx.credentialSource === 'flag' ||
458
+ ctx.credentialSource === 'env' ||
459
+ ctx.credentialSource === 'profile'
460
+ ? `not used: ${describeCredential(ctx.credentialSource, ctx.profileName)} wins`
461
+ : 'not used';
462
+ return `${stored} (${reason})`;
463
+ }
464
+ const onCloud = ctx.profileName === CLOUD_PROFILE || ctx.baseUrl === normalizeUrl(identity.cloudUrl);
465
+ return onCloud ? `not connected (run \`${identity.bin} login\`)` : null;
466
+ }
467
+ program
468
+ .command('verify')
469
+ .description('Check that the resolved instance + key work (exit 0 = ok)')
470
+ .action(async (_opts, cmd) => {
471
+ const ctx = resolveContext(cmd, io);
472
+ await requireApiKey(ctx);
473
+ const me = await ctx.client.me();
474
+ if (ctx.json)
475
+ return printJson(io, { ok: true, baseUrl: ctx.baseUrl, user: me.user });
476
+ io.out.write(`ok — ${me.user.email} (${me.role}) @ ${ctx.baseUrl}\n`);
477
+ });
478
+ program
479
+ .command('use <profile>')
480
+ .description('Switch the active profile (the instance the commands run against)')
481
+ .action(async (name, _opts, cmd) => {
482
+ const globals = cmd.optsWithGlobals();
483
+ const config = loadConfig(io.env);
484
+ if (!config.profiles[name]) {
485
+ if (name !== CLOUD_PROFILE) {
486
+ throw new CliUsageError(`Unknown profile "${name}" — run \`${identity.bin} profiles\`.`);
487
+ }
488
+ // The implicit cloud profile exists before anything is written: `use
489
+ // cloud` makes it a row so it can be the active one.
490
+ config.profiles[name] = { baseUrl: normalizeUrl(identity.cloudUrl) };
491
+ }
492
+ config.activeProfile = name;
493
+ saveConfig(io.env, config);
494
+ if (globals.json)
495
+ return printJson(io, { activeProfile: name });
496
+ io.out.write(`Active profile: ${name} (${profileUrl(config.profiles[name])})\n`);
497
+ });
498
+ program
499
+ .command('profiles')
500
+ .description('List the profiles (one per instance) and how each signs in')
501
+ .action(async (_opts, cmd) => {
502
+ const globals = cmd.optsWithGlobals();
503
+ const config = loadConfig(io.env);
504
+ const hub = activeHubProfile(io.env);
505
+ // The implicit cloud profile is listed even before it is on disk: it is
506
+ // where a clean machine runs, and where `login` with no URL signs in.
507
+ const rows = Object.entries(config.profiles);
508
+ if (!config.profiles[CLOUD_PROFILE])
509
+ rows.unshift([CLOUD_PROFILE, { baseUrl: normalizeUrl(identity.cloudUrl) }]);
510
+ const activeName = config.activeProfile ?? CLOUD_PROFILE;
511
+ const credentialOf = (p) => {
512
+ if (p.apiKey)
513
+ return { kind: 'key', text: redactKey(p.apiKey) };
514
+ const entry = hub.name ? p.connectKeys?.[hub.name] : undefined;
515
+ if (entry)
516
+ return { kind: 'hub', text: `via Antasphere${entry.email ? ` (${entry.email})` : ''}` };
517
+ const others = Object.keys(p.connectKeys ?? {});
518
+ if (others.length > 0)
519
+ return { kind: 'hub', text: `via Antasphere (login "${others.join('", "')}", not the active one)` };
520
+ return { kind: 'none', text: '(not signed in)' };
521
+ };
522
+ if (globals.json) {
523
+ return printJson(io, {
524
+ activeProfile: activeName,
525
+ hubProfile: hub.name ?? null,
526
+ profiles: Object.fromEntries(rows.map(([n, p]) => {
527
+ const cred = credentialOf(p);
528
+ return [
529
+ n,
530
+ {
531
+ baseUrl: profileUrl(p),
532
+ saved: Boolean(config.profiles[n]),
533
+ credential: cred.kind,
534
+ apiKey: p.apiKey ? redactKey(p.apiKey) : null,
535
+ email: p.email ?? (hub.name ? (p.connectKeys?.[hub.name]?.email ?? null) : null),
536
+ hubProfiles: Object.keys(p.connectKeys ?? {})
537
+ }
538
+ ];
539
+ }))
540
+ });
541
+ }
542
+ io.out.write(table(rows.map(([n, p]) => [activeName === n ? '*' : ' ', n, profileUrl(p), credentialOf(p).text])));
543
+ });
544
+ const config = program.command('config').description('Inspect or reset the CLI config file');
545
+ config
546
+ .command('show')
547
+ .description('Show the config path, active profile, and redacted keys')
548
+ .action(async (_opts, cmd) => {
549
+ const globals = cmd.optsWithGlobals();
550
+ const cfg = loadConfig(io.env);
551
+ const path = configPath(io.env);
552
+ const redacted = {
553
+ path,
554
+ activeProfile: cfg.activeProfile ?? null,
555
+ profiles: Object.fromEntries(Object.entries(cfg.profiles).map(([n, p]) => [
556
+ n,
557
+ {
558
+ baseUrl: profileUrl(p),
559
+ apiKey: p.apiKey ? redactKey(p.apiKey) : null,
560
+ email: p.email ?? null,
561
+ // Hub-connected keys (one per hub profile), redacted like the rest.
562
+ connectKeys: Object.fromEntries(Object.entries(p.connectKeys ?? {}).map(([slot, entry]) => [slot, redactKey(entry.apiKey)]))
563
+ }
564
+ ]))
565
+ };
566
+ if (globals.json)
567
+ return printJson(io, redacted);
568
+ io.out.write(`config: ${path ?? '(no HOME/XDG_CONFIG_HOME — config disabled)'}\n`);
569
+ io.out.write(`${JSON.stringify(redacted, null, 2)}\n`);
570
+ });
571
+ config
572
+ .command('clear')
573
+ .description('Delete the config file (all profiles and stored keys)')
574
+ .action(async (_opts, cmd) => {
575
+ const globals = cmd.optsWithGlobals();
576
+ clearConfig(io.env);
577
+ if (globals.json)
578
+ return printJson(io, { cleared: true });
579
+ io.out.write('Config cleared.\n');
580
+ });
581
+ }
582
+ //# sourceMappingURL=auth.js.map