@moda-ai/cli 1.12.6 → 1.13.1
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/auth-storage.d.ts +35 -0
- package/dist/auth-storage.js +159 -0
- package/dist/auth.d.ts +25 -14
- package/dist/auth.js +442 -151
- package/dist/cli-session.d.ts +26 -0
- package/dist/cli-session.js +169 -0
- package/dist/cli.js +22 -6
- package/dist/config.d.ts +4 -1
- package/dist/config.js +6 -3
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +15 -0
- package/dist/exit-funnel.js +5 -1
- package/dist/init/index.js +14 -3
- package/dist/init/tenant.d.ts +16 -2
- package/dist/init/tenant.js +182 -45
- package/dist/output.d.ts +1 -1
- package/dist/output.js +12 -1
- package/dist/profile-commands.js +81 -3
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
- package/skills/moda-cli/SKILL.md +120 -3
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CliAuthError } from './errors.js';
|
|
2
|
+
import { type AuthSessionOptions, type StoredAuth } from './auth-storage.js';
|
|
3
|
+
/**
|
|
4
|
+
* Fire-and-forget sliding-expiry renewal, invoked on every authenticated
|
|
5
|
+
* request: a no-op unless the session is a moda_ct token inside the renewal
|
|
6
|
+
* window. Never awaited by callers, never throws, never holds the process
|
|
7
|
+
* open (unref'd socket); on success the stored expires_at is rewritten in
|
|
8
|
+
* place.
|
|
9
|
+
*/
|
|
10
|
+
export declare function scheduleRenewalIfNearExpiry(session: StoredAuth, authOptions: AuthSessionOptions): void;
|
|
11
|
+
/** The CliAuthError thrown whenever a stored moda_ct session has expired. */
|
|
12
|
+
export declare function cliSessionExpiredError(): CliAuthError;
|
|
13
|
+
/**
|
|
14
|
+
* Classify a 401 for a moda_ct session. Returns the CliAuthError to throw,
|
|
15
|
+
* or null when the renew probe says the token is actually alive (the caller
|
|
16
|
+
* then surfaces the original response unchanged).
|
|
17
|
+
*/
|
|
18
|
+
export declare function classifyModaCtUnauthorized(session: StoredAuth, authOptions: AuthSessionOptions): Promise<CliAuthError | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Best-effort server-side revocation of a stored moda_ct session token,
|
|
21
|
+
* called by `moda auth logout` before local deletion. No-op for legacy
|
|
22
|
+
* auth0-kind sessions; all failures are swallowed (the local deletion is
|
|
23
|
+
* the contract).
|
|
24
|
+
*/
|
|
25
|
+
export declare function revokeCliSessionToken(options?: AuthSessionOptions): Promise<void>;
|
|
26
|
+
//# sourceMappingURL=cli-session.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/cli-session.ts — moda_ct CLI session token lifecycle: renewal
|
|
2
|
+
// (sliding expiry), revocation, and 401 classification against the
|
|
3
|
+
// /api/cli/auth/* endpoints.
|
|
4
|
+
import { request as httpRequest } from 'node:http';
|
|
5
|
+
import { request as httpsRequest } from 'node:https';
|
|
6
|
+
import { resolveModaBaseUrl } from './config.js';
|
|
7
|
+
import { CliAuthError } from './errors.js';
|
|
8
|
+
import { isModaCtSession, loadAuthSession, persistAuthSession, } from './auth-storage.js';
|
|
9
|
+
// Background sliding-expiry renewal fires when a moda_ct session has less
|
|
10
|
+
// than this long left.
|
|
11
|
+
const RENEW_WINDOW_SECONDS = 7 * 86_400;
|
|
12
|
+
const RENEW_TIMEOUT_MS = 10_000;
|
|
13
|
+
// Synchronous renew probe used to classify a 401, and best-effort revoke on
|
|
14
|
+
// logout: both must never stall a command, so they get a tight budget.
|
|
15
|
+
const RENEW_PROBE_TIMEOUT_MS = 2_000;
|
|
16
|
+
const REVOKE_TIMEOUT_MS = 2_000;
|
|
17
|
+
/**
|
|
18
|
+
* POST an Authorization-bearer request to a CLI session lifecycle endpoint
|
|
19
|
+
* (/api/cli/auth/renew or /api/cli/auth/revoke) over raw node http/https.
|
|
20
|
+
* Not fetch: the background renewal must never delay process exit, and only
|
|
21
|
+
* a raw socket can be unref'd. Rejects on network failure or timeout.
|
|
22
|
+
*/
|
|
23
|
+
function postCliSessionEndpoint(path, token, timeoutMs, opts = {}) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
let url;
|
|
26
|
+
try {
|
|
27
|
+
// Resolve the origin from the same profile/env context the session was
|
|
28
|
+
// loaded with: a `--profile staging` session's renew/revoke must hit the
|
|
29
|
+
// staging backend that minted it, not the active/default profile's.
|
|
30
|
+
url = new URL(`${resolveModaBaseUrl(opts.authOptions)}${path}`);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const requestFn = url.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
37
|
+
const req = requestFn(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
40
|
+
timeout: timeoutMs,
|
|
41
|
+
}, (res) => {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
44
|
+
res.on('end', () => {
|
|
45
|
+
resolve({
|
|
46
|
+
status: res.statusCode ?? 0,
|
|
47
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
res.on('error', reject);
|
|
51
|
+
});
|
|
52
|
+
if (opts.unrefSocket) {
|
|
53
|
+
req.on('socket', (socket) => {
|
|
54
|
+
socket.unref();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
req.on('timeout', () => {
|
|
58
|
+
req.destroy(new Error(`CLI session request timed out after ${timeoutMs}ms`));
|
|
59
|
+
});
|
|
60
|
+
req.on('error', reject);
|
|
61
|
+
req.end();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse a renew response body and rewrite the stored session's expiry in
|
|
66
|
+
* place, preserving the profile scoping the session was loaded with.
|
|
67
|
+
* Best-effort: an unreadable or incomplete body is simply ignored.
|
|
68
|
+
*/
|
|
69
|
+
function persistRenewedExpiry(session, body, authOptions) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(body);
|
|
72
|
+
const expiresAtMs = typeof parsed.expires_at === 'string' ? Date.parse(parsed.expires_at) : NaN;
|
|
73
|
+
if (!Number.isFinite(expiresAtMs))
|
|
74
|
+
return;
|
|
75
|
+
persistAuthSession({ ...session, expires_at: Math.floor(expiresAtMs / 1000) }, authOptions);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Best-effort persistence only.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Stampede guard: at most one background renewal in flight per process.
|
|
82
|
+
let renewInFlight = false;
|
|
83
|
+
/**
|
|
84
|
+
* Fire-and-forget sliding-expiry renewal, invoked on every authenticated
|
|
85
|
+
* request: a no-op unless the session is a moda_ct token inside the renewal
|
|
86
|
+
* window. Never awaited by callers, never throws, never holds the process
|
|
87
|
+
* open (unref'd socket); on success the stored expires_at is rewritten in
|
|
88
|
+
* place.
|
|
89
|
+
*/
|
|
90
|
+
export function scheduleRenewalIfNearExpiry(session, authOptions) {
|
|
91
|
+
if (!isModaCtSession(session))
|
|
92
|
+
return;
|
|
93
|
+
if (session.expires_at - Date.now() / 1000 >= RENEW_WINDOW_SECONDS)
|
|
94
|
+
return;
|
|
95
|
+
if (renewInFlight)
|
|
96
|
+
return;
|
|
97
|
+
renewInFlight = true;
|
|
98
|
+
void (async () => {
|
|
99
|
+
try {
|
|
100
|
+
const result = await postCliSessionEndpoint('/api/cli/auth/renew', session.access_token, RENEW_TIMEOUT_MS, { unrefSocket: true, authOptions });
|
|
101
|
+
if (result.status !== 200)
|
|
102
|
+
return;
|
|
103
|
+
persistRenewedExpiry(session, result.body, authOptions);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Silent by design: renewal is opportunistic, the command result must
|
|
107
|
+
// not depend on it.
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
renewInFlight = false;
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
}
|
|
114
|
+
/** The CliAuthError thrown whenever a stored moda_ct session has expired. */
|
|
115
|
+
export function cliSessionExpiredError() {
|
|
116
|
+
return new CliAuthError('The stored CLI session token has expired. Run "moda auth login" to sign in again.', { reason: 'token_expired', login_command: ['moda', 'auth', 'login'] });
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Classify a 401 for a moda_ct session. Returns the CliAuthError to throw,
|
|
120
|
+
* or null when the renew probe says the token is actually alive (the caller
|
|
121
|
+
* then surfaces the original response unchanged).
|
|
122
|
+
*/
|
|
123
|
+
export async function classifyModaCtUnauthorized(session, authOptions) {
|
|
124
|
+
const loginCommand = ['moda', 'auth', 'login'];
|
|
125
|
+
if (Date.now() / 1000 > session.expires_at) {
|
|
126
|
+
return cliSessionExpiredError();
|
|
127
|
+
}
|
|
128
|
+
let probe;
|
|
129
|
+
try {
|
|
130
|
+
probe = await postCliSessionEndpoint('/api/cli/auth/renew', session.access_token, RENEW_PROBE_TIMEOUT_MS, { authOptions });
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return new CliAuthError('The Moda API rejected the stored CLI session token, and the renewal check could not reach the server. ' +
|
|
134
|
+
'The token has not expired, so this is likely transient: retry the command instead of logging in again.', { reason: 'renew_failed', login_command: loginCommand });
|
|
135
|
+
}
|
|
136
|
+
if (probe.status === 401) {
|
|
137
|
+
return new CliAuthError('The stored CLI session token has been revoked. Run "moda auth login" to sign in again.', { reason: 'token_revoked', login_command: loginCommand });
|
|
138
|
+
}
|
|
139
|
+
if (probe.status === 200) {
|
|
140
|
+
// The token is demonstrably alive (the probe just slid its expiry) but
|
|
141
|
+
// the original request still 401'd; persist the fresh expiry and let the
|
|
142
|
+
// caller see the original response.
|
|
143
|
+
persistRenewedExpiry(session, probe.body, authOptions);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
// Any other renew status (e.g. a 5xx) is indistinguishable from a
|
|
147
|
+
// transient server problem: same retry guidance as a network failure.
|
|
148
|
+
return new CliAuthError('The Moda API rejected the stored CLI session token, and the renewal check failed ' +
|
|
149
|
+
`(HTTP ${probe.status}). The token has not expired, so this is likely transient: ` +
|
|
150
|
+
'retry the command instead of logging in again.', { reason: 'renew_failed', login_command: loginCommand });
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Best-effort server-side revocation of a stored moda_ct session token,
|
|
154
|
+
* called by `moda auth logout` before local deletion. No-op for legacy
|
|
155
|
+
* auth0-kind sessions; all failures are swallowed (the local deletion is
|
|
156
|
+
* the contract).
|
|
157
|
+
*/
|
|
158
|
+
export async function revokeCliSessionToken(options = {}) {
|
|
159
|
+
const session = loadAuthSession({ ...options, includeExpired: true });
|
|
160
|
+
if (!session || !isModaCtSession(session))
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
await postCliSessionEndpoint('/api/cli/auth/revoke', session.access_token, REVOKE_TIMEOUT_MS, { authOptions: options });
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Best-effort: logout proceeds with local deletion regardless.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=cli-session.js.map
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import { runSkillsCommand } from './skills.js';
|
|
|
12
12
|
import { runHarnessCommand } from './harness.js';
|
|
13
13
|
import { runSyncCommand } from './sync.js';
|
|
14
14
|
import { runDoctorCommand, runStatusCommand } from './doctor.js';
|
|
15
|
-
import { CliAuthError, CliCancellationError, CliInputError, CliInputRequiredError, isApiLikeError, isNetworkLikeError } from './errors.js';
|
|
15
|
+
import { CliAccessError, CliAuthError, CliCancellationError, CliInputError, CliInputRequiredError, isApiLikeError, isNetworkLikeError } from './errors.js';
|
|
16
16
|
import { buildSearchAdoptionTelemetry, isTelemetryDisabled, SEARCH_MODES, sendCliUsageTelemetry, stripTelemetryFlag, } from './telemetry.js';
|
|
17
17
|
import { isUpdateCheckDisabled, printUpdateNotice, startUpdateCheck, stripUpdateCheckFlag, } from './update-check.js';
|
|
18
18
|
import { createCommandContext } from './command-context.js';
|
|
@@ -111,6 +111,8 @@ Commands:
|
|
|
111
111
|
manifest Describe the CLI machine protocol and commands
|
|
112
112
|
doctor Diagnose local Moda setup and data flow
|
|
113
113
|
auth login Authenticate with Moda
|
|
114
|
+
auth status Show local auth session state (exit 4 when logged out); --online probes the API
|
|
115
|
+
auth token Print the raw bearer token for $(moda auth token)
|
|
114
116
|
auth logout Clear cached Moda auth
|
|
115
117
|
auth whoami Show the authenticated Moda user
|
|
116
118
|
profiles list List local Moda profiles
|
|
@@ -759,7 +761,14 @@ export const commandRegistry = createCommandRegistry([
|
|
|
759
761
|
{
|
|
760
762
|
name: 'auth',
|
|
761
763
|
description: 'Authenticate and inspect browser auth sessions',
|
|
762
|
-
examples: [
|
|
764
|
+
examples: [
|
|
765
|
+
'moda auth login',
|
|
766
|
+
'moda auth status',
|
|
767
|
+
'moda auth status --online',
|
|
768
|
+
'moda auth token',
|
|
769
|
+
'moda auth whoami',
|
|
770
|
+
'moda auth logout',
|
|
771
|
+
],
|
|
763
772
|
auth: 'none',
|
|
764
773
|
outputModes: ['human', 'json', 'agent'],
|
|
765
774
|
mutability: 'write',
|
|
@@ -1075,10 +1084,12 @@ export function classifyCliError(error) {
|
|
|
1075
1084
|
if (error instanceof z.ZodError ||
|
|
1076
1085
|
error instanceof CliInputError ||
|
|
1077
1086
|
error instanceof CliInputRequiredError ||
|
|
1078
|
-
error instanceof CliAuthError
|
|
1079
|
-
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1087
|
+
error instanceof CliAuthError ||
|
|
1088
|
+
error instanceof CliAccessError) {
|
|
1089
|
+
// Auth/input-required/access are caller-actionable states (missing
|
|
1090
|
+
// credentials, needs a choice, insufficient role), not unknown failures.
|
|
1091
|
+
// Bucket them with validation so the missing-API-key case keeps its
|
|
1092
|
+
// pre-hardening telemetry type.
|
|
1082
1093
|
return 'validation_error';
|
|
1083
1094
|
}
|
|
1084
1095
|
if (error instanceof ApiError || isApiLikeError(error)) {
|
|
@@ -1095,6 +1106,11 @@ export function printCliError(error) {
|
|
|
1095
1106
|
if (error.usage)
|
|
1096
1107
|
safeWriteStderr(`${error.usage}\n`);
|
|
1097
1108
|
}
|
|
1109
|
+
else if (error instanceof CliAccessError) {
|
|
1110
|
+
safeWriteStderr(`Error: ${error.message}\n`);
|
|
1111
|
+
if (error.guidance)
|
|
1112
|
+
safeWriteStderr(`${error.guidance}\n`);
|
|
1113
|
+
}
|
|
1098
1114
|
else if (error instanceof z.ZodError) {
|
|
1099
1115
|
const issues = error.errors.map((e) => {
|
|
1100
1116
|
const field = e.path.length ? e.path.join('.') : 'input';
|
package/dist/config.d.ts
CHANGED
|
@@ -17,8 +17,11 @@ export declare function loadCliConfig(options?: ProfileIOOptions): CliConfig;
|
|
|
17
17
|
/**
|
|
18
18
|
* Merge-and-write the persisted config. Always chmod 0600 since it holds the
|
|
19
19
|
* API key. Best-effort: never throws on chmod failure (some filesystems).
|
|
20
|
+
* `options.profile` scopes the best-effort profile sync (e.g. `moda init
|
|
21
|
+
* --profile staging`), so the key is synced to the requested profile rather
|
|
22
|
+
* than whatever profile happens to be active.
|
|
20
23
|
*/
|
|
21
|
-
export declare function saveCliConfig(update: CliConfig): void;
|
|
24
|
+
export declare function saveCliConfig(update: CliConfig, options?: ProfileIOOptions): void;
|
|
22
25
|
/**
|
|
23
26
|
* Resolve the API key: prefer the environment (so CI / explicit exports win),
|
|
24
27
|
* then fall back to the persisted `~/.moda/config.json`. Read lazily so a
|
package/dist/config.js
CHANGED
|
@@ -14,11 +14,14 @@ export function loadCliConfig(options = {}) {
|
|
|
14
14
|
/**
|
|
15
15
|
* Merge-and-write the persisted config. Always chmod 0600 since it holds the
|
|
16
16
|
* API key. Best-effort: never throws on chmod failure (some filesystems).
|
|
17
|
+
* `options.profile` scopes the best-effort profile sync (e.g. `moda init
|
|
18
|
+
* --profile staging`), so the key is synced to the requested profile rather
|
|
19
|
+
* than whatever profile happens to be active.
|
|
17
20
|
*/
|
|
18
|
-
export function saveCliConfig(update) {
|
|
19
|
-
const merged = writeLegacyCliConfig(update);
|
|
21
|
+
export function saveCliConfig(update, options = {}) {
|
|
22
|
+
const merged = writeLegacyCliConfig(update, options);
|
|
20
23
|
try {
|
|
21
|
-
syncLegacyConfigToProfile(merged);
|
|
24
|
+
syncLegacyConfigToProfile(merged, options);
|
|
22
25
|
}
|
|
23
26
|
catch {
|
|
24
27
|
// The legacy config write is the compatibility contract; profile sync is a
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,6 +2,19 @@ export declare class CliInputError extends Error {
|
|
|
2
2
|
readonly usage?: string | undefined;
|
|
3
3
|
constructor(message: string, usage?: string | undefined);
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* The authenticated user cannot perform the requested action with their
|
|
7
|
+
* current access (no organizations, or insufficient role). This is an access
|
|
8
|
+
* problem, not missing auth — the dispatcher maps it to EXIT_ERROR (1),
|
|
9
|
+
* never EXIT_AUTH_REQUIRED.
|
|
10
|
+
*/
|
|
11
|
+
export declare class CliAccessError extends Error {
|
|
12
|
+
/** Optional follow-up line telling the user how to obtain access. */
|
|
13
|
+
readonly guidance?: string;
|
|
14
|
+
constructor(message: string, options?: {
|
|
15
|
+
guidance?: string;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
5
18
|
export interface AgentInputChoice {
|
|
6
19
|
value: string;
|
|
7
20
|
label: string;
|
package/dist/errors.js
CHANGED
|
@@ -6,6 +6,21 @@ export class CliInputError extends Error {
|
|
|
6
6
|
this.name = 'CliInputError';
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* The authenticated user cannot perform the requested action with their
|
|
11
|
+
* current access (no organizations, or insufficient role). This is an access
|
|
12
|
+
* problem, not missing auth — the dispatcher maps it to EXIT_ERROR (1),
|
|
13
|
+
* never EXIT_AUTH_REQUIRED.
|
|
14
|
+
*/
|
|
15
|
+
export class CliAccessError extends Error {
|
|
16
|
+
/** Optional follow-up line telling the user how to obtain access. */
|
|
17
|
+
guidance;
|
|
18
|
+
constructor(message, options = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'CliAccessError';
|
|
21
|
+
this.guidance = options.guidance;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
9
24
|
export class CliInputRequiredError extends Error {
|
|
10
25
|
input_request;
|
|
11
26
|
constructor(message, input_request, usage) {
|
package/dist/exit-funnel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliAuthError, CliCancellationError, CliInputRequiredError, } from './errors.js';
|
|
1
|
+
import { CliAccessError, CliAuthError, CliCancellationError, CliInputRequiredError, } from './errors.js';
|
|
2
2
|
import { EXIT_AUTH_REQUIRED, EXIT_CANCELLED, EXIT_ERROR, EXIT_INPUT_REQUIRED, EXIT_OK, wasOutputCancelled, } from './output.js';
|
|
3
3
|
const finishedContexts = new WeakSet();
|
|
4
4
|
export async function finishRun(context, outcome) {
|
|
@@ -37,6 +37,10 @@ export function exitCodeForError(error) {
|
|
|
37
37
|
return EXIT_INPUT_REQUIRED;
|
|
38
38
|
if (error instanceof CliCancellationError)
|
|
39
39
|
return EXIT_CANCELLED;
|
|
40
|
+
// An access problem (missing org / insufficient role) is a hard failure,
|
|
41
|
+
// never auth-required — it stays EXIT_ERROR (the default), made explicit here.
|
|
42
|
+
if (error instanceof CliAccessError)
|
|
43
|
+
return EXIT_ERROR;
|
|
40
44
|
return EXIT_ERROR;
|
|
41
45
|
}
|
|
42
46
|
function exitCodeForOutcome(outcome) {
|
package/dist/init/index.js
CHANGED
|
@@ -44,8 +44,13 @@ export async function runInit(flags, options = {}) {
|
|
|
44
44
|
emit('welcome', 'Onboarding this project as a Moda agent harness.', 'completed');
|
|
45
45
|
phases.push({ id: 'welcome', title: 'Welcome', status: 'completed' });
|
|
46
46
|
// Step 2: Authentication
|
|
47
|
+
// Every auth touchpoint below (cached-token check, browser login, tenant
|
|
48
|
+
// setup) must share the same --profile context, or `moda init --profile X`
|
|
49
|
+
// logs in / stores under the default profile while the rest of init reads
|
|
50
|
+
// from X — the session then looks unauthenticated or uses the wrong key.
|
|
51
|
+
const profile = stringFlag(flags.profile);
|
|
47
52
|
log(renderInitStep(2, 7, 'Authentication'));
|
|
48
|
-
let token = loadToken();
|
|
53
|
+
let token = loadToken({ profile });
|
|
49
54
|
if (!token) {
|
|
50
55
|
if (json || agent) {
|
|
51
56
|
throw new CliAuthError('Browser authentication is required before non-interactive init can continue.', {
|
|
@@ -66,7 +71,7 @@ export async function runInit(flags, options = {}) {
|
|
|
66
71
|
}
|
|
67
72
|
log(renderInitNote(`Opening browser to ${action === 'signup' ? 'sign up' : 'log in'}...`, 'warning'));
|
|
68
73
|
emit('auth', `Opening browser to ${action}.`);
|
|
69
|
-
token = await login(action);
|
|
74
|
+
token = await login(action, { profile });
|
|
70
75
|
log(renderInitNote('Authentication successful'));
|
|
71
76
|
}
|
|
72
77
|
else {
|
|
@@ -77,11 +82,17 @@ export async function runInit(flags, options = {}) {
|
|
|
77
82
|
// Step 3: Tenant + API Key
|
|
78
83
|
log(renderInitStep(3, 7, 'Workspace & Profile'));
|
|
79
84
|
emit('workspace', 'Selecting workspace and creating API key.');
|
|
85
|
+
// --tenant-id flag wins over MODA_TENANT_ID; the source drives the
|
|
86
|
+
// env-shadow warning so we don't warn when the env var was the selection.
|
|
87
|
+
const tenantIdFlag = stringFlag(flagValue(flags, 'tenant-id', 'tenant_id'));
|
|
88
|
+
const tenantIdEnv = process.env.MODA_TENANT_ID;
|
|
80
89
|
const { apiKey, tenantName } = await selectTenantAndCreateKey({
|
|
81
|
-
tenantId:
|
|
90
|
+
tenantId: tenantIdFlag ?? tenantIdEnv,
|
|
91
|
+
tenantIdSource: tenantIdFlag ? 'flag' : tenantIdEnv ? 'env' : undefined,
|
|
82
92
|
nonInteractive: json || agent,
|
|
83
93
|
silent: !human,
|
|
84
94
|
resumeCommand: ['moda', 'init', ...resumeFlagsForInit(flags)],
|
|
95
|
+
profile,
|
|
85
96
|
});
|
|
86
97
|
log(renderInitNote(`Active workspace: ${tenantName}`));
|
|
87
98
|
emit('workspace', `Active workspace: ${tenantName}`, 'completed');
|
package/dist/init/tenant.d.ts
CHANGED
|
@@ -4,11 +4,25 @@ interface BootstrapResult {
|
|
|
4
4
|
tenantName: string;
|
|
5
5
|
tenantSlug: string;
|
|
6
6
|
}
|
|
7
|
-
export
|
|
7
|
+
export interface SelectTenantAndCreateKeyOptions {
|
|
8
|
+
/** Tenant id preselected via --tenant-id (flag) or MODA_TENANT_ID (env). */
|
|
8
9
|
tenantId?: string;
|
|
10
|
+
/** Where the preselected tenant id came from; drives the env-shadow warning. */
|
|
11
|
+
tenantIdSource?: 'flag' | 'env';
|
|
12
|
+
/** Non-interactive runs must never open the selection prompt. */
|
|
9
13
|
nonInteractive?: boolean;
|
|
14
|
+
/** Suppress human prose so it never interleaves with JSON envelopes on stdout. */
|
|
10
15
|
silent?: boolean;
|
|
16
|
+
/** Resume command echoed into the needs_input event for multi-tenant runs. */
|
|
11
17
|
resumeCommand?: string[];
|
|
12
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Profile context (--profile) threaded from init: scopes the auth-session
|
|
20
|
+
* reads and request origin behind authFetch, the stored-key candidate
|
|
21
|
+
* lookup, and the liveness-probe base URL to the given profile.
|
|
22
|
+
*/
|
|
23
|
+
profile?: string;
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
}
|
|
26
|
+
export declare function selectTenantAndCreateKey(options?: SelectTenantAndCreateKeyOptions): Promise<BootstrapResult>;
|
|
13
27
|
export {};
|
|
14
28
|
//# sourceMappingURL=tenant.d.ts.map
|