@moda-ai/cli 1.22.2 → 1.23.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.
@@ -0,0 +1,762 @@
1
+ import {
2
+ CliAuthError,
3
+ CliInputError,
4
+ __require,
5
+ getModaPaths,
6
+ loadProfileConfig,
7
+ resolveModaBaseUrl,
8
+ saveProfileConfig,
9
+ secretFilePathFromRef,
10
+ writeSecretJsonFile
11
+ } from "./cli-mnqa4rp6.js";
12
+
13
+ // src/auth-storage.ts
14
+ import { existsSync, readFileSync, rmSync } from "node:fs";
15
+ function tokenPath(options = {}) {
16
+ return getModaPaths(options).legacyAuthPath;
17
+ }
18
+ function authTokenRefForProfile(profile) {
19
+ return `file:secrets/auth/${profile}.json`;
20
+ }
21
+ function parseStoredAuth(value) {
22
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
23
+ return null;
24
+ }
25
+ const data = value;
26
+ if (typeof data.access_token !== "string" || typeof data.expires_at !== "number") {
27
+ return null;
28
+ }
29
+ return {
30
+ access_token: data.access_token,
31
+ expires_at: data.expires_at,
32
+ profile: typeof data.profile === "string" ? data.profile : undefined,
33
+ token_type: data.token_type === "bearer" ? "bearer" : undefined,
34
+ token_kind: data.token_kind === "moda_ct" || data.token_kind === "auth0" ? data.token_kind : undefined,
35
+ user_email: typeof data.user_email === "string" ? data.user_email : undefined
36
+ };
37
+ }
38
+ function isModaCtSession(session) {
39
+ return session.token_kind === "moda_ct" || session.access_token.startsWith("moda_ct_");
40
+ }
41
+ function isAuthSessionValid(data, now = Date.now()) {
42
+ if (!data)
43
+ return false;
44
+ return now / 1000 <= data.expires_at - 60;
45
+ }
46
+ function readStoredAuth(path) {
47
+ try {
48
+ if (!existsSync(path))
49
+ return null;
50
+ return parseStoredAuth(JSON.parse(readFileSync(path, "utf8")));
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function loadToken(options = {}) {
56
+ return loadAuthSession(options)?.access_token ?? null;
57
+ }
58
+ function loadAuthSession(options = {}) {
59
+ const profileSession = loadProfileAuthSession(options);
60
+ if (profileSession)
61
+ return profileSession;
62
+ const legacy = readStoredAuth(tokenPath(options));
63
+ if (!legacy)
64
+ return null;
65
+ if (!options.includeExpired && !isAuthSessionValid(legacy)) {
66
+ return null;
67
+ }
68
+ return legacy;
69
+ }
70
+ function loadProfileAuthSession(options) {
71
+ const profileName = options.profile ?? (options.env ?? process.env).MODA_PROFILE;
72
+ if (!profileName)
73
+ return null;
74
+ try {
75
+ const profile = loadProfileConfig(options).profiles[profileName];
76
+ const tokenRef = profile?.auth_token_ref;
77
+ if (!tokenRef)
78
+ return null;
79
+ const path = secretFilePathFromRef(tokenRef, options);
80
+ if (!path)
81
+ return null;
82
+ const session = readStoredAuth(path);
83
+ if (!session)
84
+ return null;
85
+ if (!options.includeExpired && !isAuthSessionValid(session))
86
+ return null;
87
+ return session;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+ function resolveStorageProfile(options) {
93
+ return options.profile ?? (options.env ?? process.env).MODA_PROFILE;
94
+ }
95
+ function saveAuthSession(accessToken, expiresIn, options = {}, extras = {}) {
96
+ return persistAuthSession({
97
+ access_token: accessToken,
98
+ expires_at: Math.floor(Date.now() / 1000) + expiresIn,
99
+ profile: resolveStorageProfile(options),
100
+ token_type: "bearer",
101
+ ...extras.token_kind ? { token_kind: extras.token_kind } : {},
102
+ ...extras.user_email ? { user_email: extras.user_email } : {}
103
+ }, options);
104
+ }
105
+ function persistAuthSession(data, options = {}) {
106
+ const profile = data.profile;
107
+ if (profile) {
108
+ const ref = authTokenRefForProfile(profile);
109
+ const path = secretFilePathFromRef(ref, options);
110
+ if (!path)
111
+ throw new Error(`Unsupported auth token reference: ${ref}`);
112
+ const config = loadProfileConfig(options);
113
+ const existing = config.profiles[profile];
114
+ if (!existing) {
115
+ throw new Error(`Moda profile '${profile}' does not exist.`);
116
+ }
117
+ writeSecretJsonFile(path, data);
118
+ saveProfileConfig({
119
+ ...config,
120
+ profiles: {
121
+ ...config.profiles,
122
+ [profile]: {
123
+ ...existing,
124
+ auth_token_ref: ref
125
+ }
126
+ }
127
+ }, options);
128
+ return data;
129
+ }
130
+ writeSecretJsonFile(tokenPath(options), data);
131
+ return data;
132
+ }
133
+ function clearAuthSession(options = {}) {
134
+ const profile = options.profile ?? (options.env ?? process.env).MODA_PROFILE;
135
+ if (profile) {
136
+ const ref = authTokenRefForProfile(profile);
137
+ const path = secretFilePathFromRef(ref, options);
138
+ if (path)
139
+ rmSync(path, { force: true });
140
+ const config = loadProfileConfig(options);
141
+ const existing = config.profiles[profile];
142
+ if (existing) {
143
+ const { auth_token_ref: _drop, ...profileWithoutToken } = existing;
144
+ saveProfileConfig({
145
+ ...config,
146
+ profiles: {
147
+ ...config.profiles,
148
+ [profile]: profileWithoutToken
149
+ }
150
+ }, options);
151
+ }
152
+ return;
153
+ }
154
+ rmSync(tokenPath(options), { force: true });
155
+ }
156
+
157
+ // src/auth.ts
158
+ import { createServer } from "node:http";
159
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
160
+ import { hostname } from "node:os";
161
+ import { createInterface } from "node:readline";
162
+
163
+ // src/cli-session.ts
164
+ import { request as httpRequest } from "node:http";
165
+ import { request as httpsRequest } from "node:https";
166
+ var RENEW_WINDOW_SECONDS = 7 * 86400;
167
+ var RENEW_TIMEOUT_MS = 1e4;
168
+ var RENEW_PROBE_TIMEOUT_MS = 2000;
169
+ var REVOKE_TIMEOUT_MS = 2000;
170
+ function postCliSessionEndpoint(path, token, timeoutMs, opts = {}) {
171
+ return new Promise((resolve, reject) => {
172
+ let url;
173
+ try {
174
+ url = new URL(`${resolveModaBaseUrl(opts.authOptions)}${path}`);
175
+ } catch (err) {
176
+ reject(err instanceof Error ? err : new Error(String(err)));
177
+ return;
178
+ }
179
+ const requestFn = url.protocol === "https:" ? httpsRequest : httpRequest;
180
+ const req = requestFn(url, {
181
+ method: "POST",
182
+ headers: { Authorization: `Bearer ${token}` },
183
+ timeout: timeoutMs
184
+ }, (res) => {
185
+ const chunks = [];
186
+ res.on("data", (chunk) => chunks.push(chunk));
187
+ res.on("end", () => {
188
+ resolve({
189
+ status: res.statusCode ?? 0,
190
+ body: Buffer.concat(chunks).toString("utf8")
191
+ });
192
+ });
193
+ res.on("error", reject);
194
+ });
195
+ if (opts.unrefSocket) {
196
+ req.on("socket", (socket) => {
197
+ socket.unref();
198
+ });
199
+ }
200
+ req.on("timeout", () => {
201
+ req.destroy(new Error(`CLI session request timed out after ${timeoutMs}ms`));
202
+ });
203
+ req.on("error", reject);
204
+ req.end();
205
+ });
206
+ }
207
+ function persistRenewedExpiry(session, body, authOptions) {
208
+ try {
209
+ const parsed = JSON.parse(body);
210
+ const expiresAtMs = typeof parsed.expires_at === "string" ? Date.parse(parsed.expires_at) : NaN;
211
+ if (!Number.isFinite(expiresAtMs))
212
+ return;
213
+ persistAuthSession({ ...session, expires_at: Math.floor(expiresAtMs / 1000) }, authOptions);
214
+ } catch {}
215
+ }
216
+ var renewInFlight = false;
217
+ function scheduleRenewalIfNearExpiry(session, authOptions) {
218
+ if (!isModaCtSession(session))
219
+ return;
220
+ if (session.expires_at - Date.now() / 1000 >= RENEW_WINDOW_SECONDS)
221
+ return;
222
+ if (renewInFlight)
223
+ return;
224
+ renewInFlight = true;
225
+ (async () => {
226
+ try {
227
+ const result = await postCliSessionEndpoint("/api/cli/auth/renew", session.access_token, RENEW_TIMEOUT_MS, { unrefSocket: true, authOptions });
228
+ if (result.status !== 200)
229
+ return;
230
+ persistRenewedExpiry(session, result.body, authOptions);
231
+ } catch {} finally {
232
+ renewInFlight = false;
233
+ }
234
+ })();
235
+ }
236
+ function cliSessionExpiredError() {
237
+ 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"] });
238
+ }
239
+ async function classifyModaCtUnauthorized(session, authOptions) {
240
+ const loginCommand = ["moda", "auth", "login"];
241
+ if (Date.now() / 1000 > session.expires_at) {
242
+ return cliSessionExpiredError();
243
+ }
244
+ let probe;
245
+ try {
246
+ probe = await postCliSessionEndpoint("/api/cli/auth/renew", session.access_token, RENEW_PROBE_TIMEOUT_MS, { authOptions });
247
+ } catch {
248
+ return new CliAuthError("The Moda API rejected the stored CLI session token, and the renewal check could not reach the server. " + "The token has not expired, so this is likely transient: retry the command instead of logging in again.", { reason: "renew_failed", login_command: loginCommand });
249
+ }
250
+ if (probe.status === 401) {
251
+ 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 });
252
+ }
253
+ if (probe.status === 200) {
254
+ persistRenewedExpiry(session, probe.body, authOptions);
255
+ return null;
256
+ }
257
+ return new CliAuthError("The Moda API rejected the stored CLI session token, and the renewal check failed " + `(HTTP ${probe.status}). The token has not expired, so this is likely transient: ` + "retry the command instead of logging in again.", { reason: "renew_failed", login_command: loginCommand });
258
+ }
259
+ async function revokeCliSessionToken(options = {}) {
260
+ const session = loadAuthSession({ ...options, includeExpired: true });
261
+ if (!session || !isModaCtSession(session))
262
+ return;
263
+ try {
264
+ await postCliSessionEndpoint("/api/cli/auth/revoke", session.access_token, REVOKE_TIMEOUT_MS, { authOptions: options });
265
+ } catch {}
266
+ }
267
+
268
+ // src/auth.ts
269
+ var CALLBACK_PORT = 8432;
270
+ var CALLBACK_PORT_LADDER = [CALLBACK_PORT, 8433, 8434, 8435];
271
+ var DEFAULT_AUTH_TIMEOUT_MS = 600000;
272
+ var AUTH_TIMEOUT_MIN_MS = 30000;
273
+ var AUTH_TIMEOUT_MAX_MS = 30 * 60000;
274
+ var DEFAULT_EXPIRES_IN_SECONDS = 86400;
275
+ var EXCHANGE_TIMEOUT_MS = 1e4;
276
+ function resolveAuthTimeoutMs(options = {}) {
277
+ if (typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs)) {
278
+ return Math.max(1, Math.floor(options.timeoutMs));
279
+ }
280
+ const env = options.env ?? process.env;
281
+ const raw = env.MODA_AUTH_TIMEOUT_MS?.trim();
282
+ if (raw) {
283
+ const parsed = Number.parseInt(raw, 10);
284
+ if (Number.isFinite(parsed) && parsed > 0) {
285
+ return Math.min(AUTH_TIMEOUT_MAX_MS, Math.max(AUTH_TIMEOUT_MIN_MS, parsed));
286
+ }
287
+ }
288
+ return DEFAULT_AUTH_TIMEOUT_MS;
289
+ }
290
+ function escapeHtml(value) {
291
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
292
+ }
293
+ function safeEqual(a, b) {
294
+ const aBuf = Buffer.from(a);
295
+ const bBuf = Buffer.from(b);
296
+ if (aBuf.length !== bBuf.length)
297
+ return false;
298
+ return timingSafeEqual(aBuf, bBuf);
299
+ }
300
+ function looksLikeOneTimeCode(value) {
301
+ return /^[A-Za-z0-9_-]{32,96}$/.test(value);
302
+ }
303
+
304
+ class CodeExchangeError extends Error {
305
+ reason;
306
+ constructor(message, reason) {
307
+ super(message);
308
+ this.reason = reason;
309
+ this.name = "CodeExchangeError";
310
+ }
311
+ }
312
+ function exchangeFailureReason(error) {
313
+ return error instanceof CodeExchangeError ? error.reason : "exchange_failed";
314
+ }
315
+ function computePkceChallenge(codeVerifier) {
316
+ return createHash("sha256").update(codeVerifier).digest("base64url");
317
+ }
318
+ function generatePkceVerifier() {
319
+ return randomBytes(32).toString("base64url");
320
+ }
321
+ var cachedLoginContext = null;
322
+ function getCachedLoginContext(options = {}) {
323
+ if (!cachedLoginContext)
324
+ return null;
325
+ if (cachedLoginContext.profile !== resolveStorageProfile(options))
326
+ return null;
327
+ return cachedLoginContext.context;
328
+ }
329
+ var SUCCESS_HTML = `<!DOCTYPE html>
330
+ <html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#fafafa">
331
+ <div style="text-align:center">
332
+ <h2>Authenticated</h2>
333
+ <p>You can close this tab and return to your terminal.</p>
334
+ </div>
335
+ <script>setTimeout(()=>window.close(),2000)</script>
336
+ </body></html>`;
337
+ var FRAGMENT_EXTRACTOR_HTML = `<!DOCTYPE html>
338
+ <html><body>
339
+ <script>
340
+ var h = window.location.hash.substring(1);
341
+ if (h) {
342
+ window.location.replace('/callback-token?' + h);
343
+ } else {
344
+ document.body.innerText = 'No token received.';
345
+ }
346
+ </script>
347
+ </body></html>`;
348
+ async function openAuthUrl(authUrl) {
349
+ const open = (await import("open")).default;
350
+ await open(authUrl);
351
+ }
352
+ function closeAuthServer(server) {
353
+ server.close((err) => {
354
+ if (err && err.code !== "ERR_SERVER_NOT_RUNNING") {}
355
+ });
356
+ server.closeIdleConnections?.();
357
+ }
358
+ async function login(screen = "login", options = {}) {
359
+ return new Promise((resolve, reject) => {
360
+ cachedLoginContext = null;
361
+ let settled = false;
362
+ const portsToTry = options.callbackPort !== undefined ? [options.callbackPort] : [...CALLBACK_PORT_LADDER];
363
+ let portIndex = 0;
364
+ let localPort = portsToTry[0];
365
+ let timeout;
366
+ let promptInput;
367
+ let promptStream;
368
+ let promptStreamIsTTY = false;
369
+ let pendingBarePaste = null;
370
+ let exchangeInFlight = false;
371
+ const state = randomBytes(16).toString("hex");
372
+ const codeVerifier = generatePkceVerifier();
373
+ const codeChallenge = computePkceChallenge(codeVerifier);
374
+ const storageOptions = {
375
+ home: options.home,
376
+ profile: options.profile,
377
+ now: options.now,
378
+ env: options.env
379
+ };
380
+ const targetProfile = options.profile ?? (options.env ?? process.env).MODA_PROFILE;
381
+ if (targetProfile && !loadProfileConfig(storageOptions).profiles[targetProfile]) {
382
+ reject(new CliInputError(`Moda profile '${targetProfile}' does not exist.`, 'Create it with "moda profiles create <name>" or check "moda profiles list".'));
383
+ return;
384
+ }
385
+ const server = createServer(async (req, res) => {
386
+ if (settled) {
387
+ res.writeHead(410, { Connection: "close" });
388
+ res.end("Login attempt already completed");
389
+ return;
390
+ }
391
+ const url = new URL(req.url || "/", `http://localhost:${localPort}`);
392
+ if (url.pathname === "/callback") {
393
+ const code = url.searchParams.get("code");
394
+ if (code) {
395
+ const callbackState2 = url.searchParams.get("state");
396
+ if (!callbackState2 || !safeEqual(callbackState2, state)) {
397
+ res.writeHead(400, { "Content-Type": "text/html", Connection: "close" });
398
+ res.end("<html><body><h2>Error</h2><p>Invalid authentication state.</p></body></html>", () => {
399
+ fail(new Error("Invalid authentication state."));
400
+ });
401
+ return;
402
+ }
403
+ if (exchangeInFlight) {
404
+ res.writeHead(409, { Connection: "close" });
405
+ res.end("Login code exchange already in progress");
406
+ return;
407
+ }
408
+ exchangeInFlight = true;
409
+ (async () => {
410
+ try {
411
+ const cliToken = await exchangeCodeForSession(code);
412
+ res.writeHead(302, {
413
+ Location: `${resolveModaBaseUrl(storageOptions)}/auth/cli/done?status=success`,
414
+ Connection: "close"
415
+ });
416
+ res.end(() => {
417
+ succeed(cliToken);
418
+ });
419
+ } catch (err) {
420
+ exchangeInFlight = false;
421
+ const error2 = err instanceof Error ? err : new Error(String(err));
422
+ res.writeHead(302, {
423
+ Location: `${resolveModaBaseUrl(storageOptions)}/auth/cli/done?status=error&reason=${exchangeFailureReason(err)}`,
424
+ Connection: "close"
425
+ });
426
+ res.end(() => {
427
+ fail(error2);
428
+ });
429
+ }
430
+ })();
431
+ return;
432
+ }
433
+ res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
434
+ res.end(FRAGMENT_EXTRACTOR_HTML);
435
+ return;
436
+ }
437
+ if (url.pathname !== "/callback-token") {
438
+ res.writeHead(404, { Connection: "close" });
439
+ res.end("Not found");
440
+ return;
441
+ }
442
+ const token = url.searchParams.get("token");
443
+ const expiresIn = url.searchParams.get("expires_in");
444
+ const error = url.searchParams.get("error");
445
+ const callbackState = url.searchParams.get("state");
446
+ if (!callbackState || !safeEqual(callbackState, state)) {
447
+ res.writeHead(400, { "Content-Type": "text/html", Connection: "close" });
448
+ res.end("<html><body><h2>Error</h2><p>Invalid authentication state.</p></body></html>", () => {
449
+ fail(new Error("Invalid authentication state."));
450
+ });
451
+ return;
452
+ }
453
+ if (error) {
454
+ res.writeHead(400, { "Content-Type": "text/html", Connection: "close" });
455
+ res.end(`<html><body><h2>Error</h2><p>${escapeHtml(error)}</p></body></html>`, () => {
456
+ fail(new Error(`Authentication error: ${error}`));
457
+ });
458
+ return;
459
+ }
460
+ if (!token) {
461
+ res.writeHead(400, { "Content-Type": "text/html", Connection: "close" });
462
+ res.end("<html><body><h2>Error</h2><p>No token received.</p></body></html>", () => {
463
+ fail(new Error("No token received"));
464
+ });
465
+ return;
466
+ }
467
+ try {
468
+ saveAuthSession(token, parseInt(expiresIn || String(DEFAULT_EXPIRES_IN_SECONDS), 10), storageOptions);
469
+ } catch (err) {
470
+ res.writeHead(500, { "Content-Type": "text/html", Connection: "close" });
471
+ res.end("<html><body><h2>Error</h2><p>Authenticated, but the CLI could not store the token. Return to your terminal.</p></body></html>", () => {
472
+ fail(err instanceof Error ? err : new Error(String(err)));
473
+ });
474
+ return;
475
+ }
476
+ res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
477
+ res.end(SUCCESS_HTML, () => {
478
+ succeed(token);
479
+ });
480
+ });
481
+ function removeSignalHandlers() {
482
+ process.off("SIGINT", handleSigint);
483
+ process.off("SIGTERM", handleSigterm);
484
+ }
485
+ function closePromptInput() {
486
+ if (!promptInput)
487
+ return;
488
+ promptInput.close();
489
+ promptInput = undefined;
490
+ if (promptStream === process.stdin) {
491
+ process.stdin.pause();
492
+ }
493
+ promptStream = undefined;
494
+ }
495
+ function finish(callback) {
496
+ if (settled)
497
+ return;
498
+ settled = true;
499
+ if (timeout)
500
+ clearTimeout(timeout);
501
+ removeSignalHandlers();
502
+ closePromptInput();
503
+ closeAuthServer(server);
504
+ callback();
505
+ }
506
+ function succeed(token) {
507
+ finish(() => resolve(token));
508
+ }
509
+ function fail(error) {
510
+ finish(() => reject(error));
511
+ }
512
+ function handleSigint() {
513
+ fail(new Error("Authentication cancelled."));
514
+ }
515
+ function handleSigterm() {
516
+ fail(new Error("Authentication terminated."));
517
+ }
518
+ function completeWithToken(token, expiresIn) {
519
+ try {
520
+ saveAuthSession(token, expiresIn, storageOptions);
521
+ } catch (err) {
522
+ fail(err instanceof Error ? err : new Error(String(err)));
523
+ return;
524
+ }
525
+ succeed(token);
526
+ }
527
+ async function exchangeCodeForSession(code) {
528
+ let response;
529
+ try {
530
+ response = await fetch(`${resolveModaBaseUrl(storageOptions)}/api/cli/auth/exchange`, {
531
+ method: "POST",
532
+ headers: { "Content-Type": "application/json" },
533
+ body: JSON.stringify({
534
+ code,
535
+ code_verifier: codeVerifier,
536
+ label: hostname()
537
+ }),
538
+ redirect: "manual",
539
+ signal: AbortSignal.timeout(EXCHANGE_TIMEOUT_MS)
540
+ });
541
+ } catch {
542
+ throw new CodeExchangeError("Could not reach the Moda API to exchange the login code. Check your network and try again.", "unreachable");
543
+ }
544
+ if (response.status >= 300 && response.status < 400) {
545
+ throw new CodeExchangeError(`The Moda API redirected the login code exchange (HTTP ${response.status}). Refusing to re-send the login code across a redirect. Check MODA_BASE_URL (use https://moda.dev, not www.moda.dev) and try again.`, "exchange_failed");
546
+ }
547
+ if (response.status === 401) {
548
+ throw new CodeExchangeError("The login code was rejected (invalid, expired, or already used). Run the login command again.", "code_rejected");
549
+ }
550
+ if (!response.ok) {
551
+ throw new CodeExchangeError(`Login code exchange failed (HTTP ${response.status}). Try again.`, "exchange_failed");
552
+ }
553
+ let payload;
554
+ try {
555
+ payload = await response.json();
556
+ } catch {
557
+ throw new CodeExchangeError("The login code exchange returned an unreadable response. Try again.", "exchange_failed");
558
+ }
559
+ const cliToken = typeof payload.cli_token === "string" ? payload.cli_token : undefined;
560
+ const expiresAtMs = typeof payload.expires_at === "string" ? Date.parse(payload.expires_at) : NaN;
561
+ if (!cliToken || !Number.isFinite(expiresAtMs)) {
562
+ throw new CodeExchangeError("The login code exchange returned an incomplete response. Try again.", "exchange_failed");
563
+ }
564
+ const userEmail = typeof payload.user?.email === "string" ? payload.user.email : undefined;
565
+ persistAuthSession({
566
+ access_token: cliToken,
567
+ expires_at: Math.floor(expiresAtMs / 1000),
568
+ profile: resolveStorageProfile(storageOptions),
569
+ token_type: "bearer",
570
+ token_kind: "moda_ct",
571
+ ...userEmail ? { user_email: userEmail } : {}
572
+ }, storageOptions);
573
+ cachedLoginContext = {
574
+ profile: resolveStorageProfile(storageOptions),
575
+ context: {
576
+ user: {
577
+ email: userEmail ?? "",
578
+ name: typeof payload.user?.name === "string" ? payload.user.name : null
579
+ },
580
+ tenants: Array.isArray(payload.tenants) ? payload.tenants : []
581
+ }
582
+ };
583
+ return cliToken;
584
+ }
585
+ async function completeWithCode(code) {
586
+ if (exchangeInFlight)
587
+ return;
588
+ exchangeInFlight = true;
589
+ try {
590
+ const cliToken = await exchangeCodeForSession(code);
591
+ succeed(cliToken);
592
+ } catch (err) {
593
+ exchangeInFlight = false;
594
+ fail(err instanceof Error ? err : new Error(String(err)));
595
+ }
596
+ }
597
+ function handlePastedLine(line) {
598
+ if (settled)
599
+ return;
600
+ const trimmed = line.trim();
601
+ if (pendingBarePaste !== null) {
602
+ const pending = pendingBarePaste;
603
+ pendingBarePaste = null;
604
+ if (/^y(es)?$/i.test(trimmed)) {
605
+ if (pending.kind === "code") {
606
+ completeWithCode(pending.value);
607
+ } else {
608
+ completeWithToken(pending.value, DEFAULT_EXPIRES_IN_SECONDS);
609
+ }
610
+ } else {
611
+ fail(new Error(pending.kind === "code" ? "Pasted code rejected: state cannot be verified." : "Bare token rejected: state cannot be verified."));
612
+ }
613
+ return;
614
+ }
615
+ if (!trimmed)
616
+ return;
617
+ if (/^https?:\/\//i.test(trimmed)) {
618
+ let pastedUrl;
619
+ try {
620
+ pastedUrl = new URL(trimmed);
621
+ } catch {
622
+ fail(new Error("Could not parse the pasted URL. Paste the full callback URL from the browser."));
623
+ return;
624
+ }
625
+ const params = pastedUrl.hash.length > 1 ? new URLSearchParams(pastedUrl.hash.slice(1)) : pastedUrl.searchParams;
626
+ const token = params.get("token");
627
+ const code = params.get("code");
628
+ const expiresIn = params.get("expires_in");
629
+ const pastedState = params.get("state");
630
+ if (!pastedState || !safeEqual(pastedState, state)) {
631
+ fail(new Error("Invalid authentication state."));
632
+ return;
633
+ }
634
+ if (token) {
635
+ completeWithToken(token, parseInt(expiresIn || String(DEFAULT_EXPIRES_IN_SECONDS), 10));
636
+ return;
637
+ }
638
+ if (code) {
639
+ completeWithCode(code);
640
+ return;
641
+ }
642
+ fail(new Error("No token received"));
643
+ return;
644
+ }
645
+ const kind = looksLikeOneTimeCode(trimmed) ? "code" : "token";
646
+ if (promptStreamIsTTY) {
647
+ pendingBarePaste = { kind, value: trimmed };
648
+ console.error(kind === "code" ? " State cannot be verified for a pasted code. Continue? [y/N]" : " State cannot be verified for a bare token. Continue? [y/N]");
649
+ return;
650
+ }
651
+ fail(new Error(kind === "code" ? "State cannot be verified for a pasted code in non-interactive mode. Paste the full callback URL instead." : "State cannot be verified for a bare token in non-interactive mode. Paste the full callback URL instead."));
652
+ }
653
+ function attachPromptInput() {
654
+ if (settled || promptInput)
655
+ return;
656
+ const stream = options.promptStream ?? (process.stdin && (process.stdin.isTTY || !process.stdin.destroyed) ? process.stdin : undefined);
657
+ if (!stream)
658
+ return;
659
+ promptStream = stream;
660
+ promptStreamIsTTY = Boolean(stream.isTTY);
661
+ promptInput = createInterface({ input: stream, terminal: false });
662
+ promptInput.on("line", handlePastedLine);
663
+ stream.unref?.();
664
+ }
665
+ timeout = setTimeout(() => {
666
+ fail(new Error("Authentication timed out. Please try again."));
667
+ }, resolveAuthTimeoutMs({ timeoutMs: options.timeoutMs, env: options.env }));
668
+ server.on("error", (err) => {
669
+ if (settled)
670
+ return;
671
+ if (err.code === "EADDRINUSE") {
672
+ portIndex += 1;
673
+ if (portIndex < portsToTry.length) {
674
+ localPort = portsToTry[portIndex];
675
+ server.listen(localPort, "127.0.0.1");
676
+ return;
677
+ }
678
+ if (options.callbackPort !== undefined) {
679
+ fail(new Error(`Port ${options.callbackPort} is in use. Close the process using it and try again.`));
680
+ } else {
681
+ fail(new Error(`Ports ${CALLBACK_PORT_LADDER.join(", ")} are all in use. Close a process using one of them and try again.`));
682
+ }
683
+ } else {
684
+ fail(err);
685
+ }
686
+ });
687
+ async function onListening() {
688
+ if (settled)
689
+ return;
690
+ const address = server.address();
691
+ if (address && typeof address === "object") {
692
+ localPort = address.port;
693
+ }
694
+ const authUrl = `${resolveModaBaseUrl(storageOptions)}/auth/cli?port=${localPort}&screen=${screen}&state=${state}&code_challenge=${codeChallenge}&challenge_method=S256`;
695
+ attachPromptInput();
696
+ try {
697
+ await (options.openUrl ?? openAuthUrl)(authUrl);
698
+ } catch {}
699
+ console.error(` Open this URL in your browser if it did not open automatically:
700
+ ${authUrl}`);
701
+ console.error(` Paste the callback URL or code here if the browser can't reach this machine
702
+ (if the browser page failed to redirect, copy the URL from the browser's address bar):`);
703
+ }
704
+ server.once("listening", onListening);
705
+ server.listen(localPort, "127.0.0.1");
706
+ process.once("SIGINT", handleSigint);
707
+ process.once("SIGTERM", handleSigterm);
708
+ });
709
+ }
710
+ async function authFetch(path, options = {}, authOptions = {}) {
711
+ const session = loadAuthSession(authOptions);
712
+ if (!session) {
713
+ const expired = loadAuthSession({ ...authOptions, includeExpired: true });
714
+ if (expired && isModaCtSession(expired)) {
715
+ throw cliSessionExpiredError();
716
+ }
717
+ throw new CliAuthError('Not authenticated. Run "moda init" to log in.', {
718
+ reason: "no_stored_credentials",
719
+ login_command: ["moda", "init"]
720
+ });
721
+ }
722
+ const token = session.access_token;
723
+ scheduleRenewalIfNearExpiry(session, authOptions);
724
+ const headers = new Headers(options.headers);
725
+ headers.set("Authorization", `Bearer ${token}`);
726
+ if (!headers.has("Content-Type") && options.body) {
727
+ headers.set("Content-Type", "application/json");
728
+ }
729
+ const baseUrl = resolveModaBaseUrl(authOptions);
730
+ const origin = new URL(baseUrl).origin;
731
+ let url = `${baseUrl}${path}`;
732
+ const MAX_REDIRECTS = 5;
733
+ for (let hop = 0;; hop++) {
734
+ const response = await fetch(url, { ...options, headers, redirect: "manual" });
735
+ if (response.status < 300 || response.status >= 400) {
736
+ if (response.status === 401 && isModaCtSession(session)) {
737
+ const authError = await classifyModaCtUnauthorized(session, authOptions);
738
+ if (authError)
739
+ throw authError;
740
+ }
741
+ return response;
742
+ }
743
+ const location = response.headers.get("location");
744
+ const target = location ? safeResolveUrl(location, url) : null;
745
+ if (!target || target.origin !== origin) {
746
+ throw new Error(`Moda API redirected (${response.status}) to ${location ?? "(unknown)"}. Cross-host redirects strip Authorization and break moda init. Use MODA_BASE_URL=https://moda.dev (not www.moda.dev).`);
747
+ }
748
+ if (hop >= MAX_REDIRECTS) {
749
+ throw new Error(`Moda API exceeded ${MAX_REDIRECTS} same-host redirects for ${path}.`);
750
+ }
751
+ url = target.toString();
752
+ }
753
+ }
754
+ function safeResolveUrl(location, base) {
755
+ try {
756
+ return new URL(location, base);
757
+ } catch {
758
+ return null;
759
+ }
760
+ }
761
+
762
+ export { isAuthSessionValid, loadToken, loadAuthSession, clearAuthSession, revokeCliSessionToken, getCachedLoginContext, login, authFetch };