@agent-relay/daemon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-manager.d.ts +134 -0
- package/dist/agent-manager.d.ts.map +1 -0
- package/dist/agent-manager.js +578 -0
- package/dist/agent-manager.js.map +1 -0
- package/dist/agent-registry.d.ts +99 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +213 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/agent-signing.d.ts +158 -0
- package/dist/agent-signing.d.ts.map +1 -0
- package/dist/agent-signing.js +523 -0
- package/dist/agent-signing.js.map +1 -0
- package/dist/api.d.ts +106 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +876 -0
- package/dist/api.js.map +1 -0
- package/dist/auth.d.ts +94 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +197 -0
- package/dist/auth.js.map +1 -0
- package/dist/channel-membership-store.d.ts +55 -0
- package/dist/channel-membership-store.d.ts.map +1 -0
- package/dist/channel-membership-store.js +176 -0
- package/dist/channel-membership-store.js.map +1 -0
- package/dist/cli-auth.d.ts +89 -0
- package/dist/cli-auth.d.ts.map +1 -0
- package/dist/cli-auth.js +792 -0
- package/dist/cli-auth.js.map +1 -0
- package/dist/cloud-sync.d.ts +150 -0
- package/dist/cloud-sync.d.ts.map +1 -0
- package/dist/cloud-sync.js +446 -0
- package/dist/cloud-sync.js.map +1 -0
- package/dist/connection.d.ts +130 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +438 -0
- package/dist/connection.js.map +1 -0
- package/dist/consensus-integration.d.ts +167 -0
- package/dist/consensus-integration.d.ts.map +1 -0
- package/dist/consensus-integration.js +371 -0
- package/dist/consensus-integration.js.map +1 -0
- package/dist/consensus.d.ts +271 -0
- package/dist/consensus.d.ts.map +1 -0
- package/dist/consensus.js +632 -0
- package/dist/consensus.js.map +1 -0
- package/dist/delivery-tracker.d.ts +34 -0
- package/dist/delivery-tracker.d.ts.map +1 -0
- package/dist/delivery-tracker.js +104 -0
- package/dist/delivery-tracker.js.map +1 -0
- package/dist/enhanced-features.d.ts +118 -0
- package/dist/enhanced-features.d.ts.map +1 -0
- package/dist/enhanced-features.js +176 -0
- package/dist/enhanced-features.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/index.d.ts +73 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +241 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/orchestrator.d.ts +217 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +1143 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/rate-limiter.d.ts +68 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +130 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/registry.d.ts +9 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +9 -0
- package/dist/registry.js.map +1 -0
- package/dist/relay-ledger.d.ts +261 -0
- package/dist/relay-ledger.d.ts.map +1 -0
- package/dist/relay-ledger.js +532 -0
- package/dist/relay-ledger.js.map +1 -0
- package/dist/relay-watchdog.d.ts +125 -0
- package/dist/relay-watchdog.d.ts.map +1 -0
- package/dist/relay-watchdog.js +611 -0
- package/dist/relay-watchdog.js.map +1 -0
- package/dist/repo-manager.d.ts +116 -0
- package/dist/repo-manager.d.ts.map +1 -0
- package/dist/repo-manager.js +384 -0
- package/dist/repo-manager.js.map +1 -0
- package/dist/router.d.ts +370 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +1437 -0
- package/dist/router.js.map +1 -0
- package/dist/server.d.ts +174 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1001 -0
- package/dist/server.js.map +1 -0
- package/dist/spawn-manager.d.ts +78 -0
- package/dist/spawn-manager.d.ts.map +1 -0
- package/dist/spawn-manager.js +165 -0
- package/dist/spawn-manager.js.map +1 -0
- package/dist/sync-queue.d.ts +116 -0
- package/dist/sync-queue.d.ts.map +1 -0
- package/dist/sync-queue.js +361 -0
- package/dist/sync-queue.js.map +1 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-manager.d.ts +80 -0
- package/dist/workspace-manager.d.ts.map +1 -0
- package/dist/workspace-manager.js +314 -0
- package/dist/workspace-manager.js.map +1 -0
- package/package.json +52 -0
package/dist/cli-auth.js
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Auth Handler for Workspace Daemon
|
|
3
|
+
*
|
|
4
|
+
* Handles CLI-based authentication (claude, codex, etc.) via PTY.
|
|
5
|
+
* Runs inside the workspace container where CLI tools are installed.
|
|
6
|
+
*
|
|
7
|
+
* Uses relay-pty binary for PTY emulation, providing better Node.js
|
|
8
|
+
* version compatibility by avoiding native module compilation.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import * as fs from 'fs/promises';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import * as crypto from 'crypto';
|
|
17
|
+
import { createLogger } from '@agent-relay/resiliency';
|
|
18
|
+
import { CLI_AUTH_CONFIG, stripAnsiCodes, matchesSuccessPattern, findMatchingPrompt, findMatchingError, getSupportedProviders, } from '@agent-relay/config/cli-auth-config';
|
|
19
|
+
import { getUserDirectoryService } from '@agent-relay/user-directory';
|
|
20
|
+
// Get the directory where this module is located
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
const logger = createLogger('cli-auth');
|
|
24
|
+
// Re-export for consumers
|
|
25
|
+
export { CLI_AUTH_CONFIG, getSupportedProviders };
|
|
26
|
+
/**
|
|
27
|
+
* Find the relay-pty binary path.
|
|
28
|
+
* Returns null if not found.
|
|
29
|
+
*/
|
|
30
|
+
function findRelayPtyBinary() {
|
|
31
|
+
// Get the project root (four levels up from packages/daemon/dist/)
|
|
32
|
+
// packages/daemon/dist/ -> packages/daemon -> packages -> project root
|
|
33
|
+
const projectRoot = join(__dirname, '..', '..', '..', '..');
|
|
34
|
+
const candidates = [
|
|
35
|
+
// Primary: installed by postinstall from platform-specific binary
|
|
36
|
+
join(projectRoot, 'bin', 'relay-pty'),
|
|
37
|
+
// Development: local Rust build
|
|
38
|
+
join(projectRoot, 'relay-pty', 'target', 'release', 'relay-pty'),
|
|
39
|
+
join(projectRoot, 'relay-pty', 'target', 'debug', 'relay-pty'),
|
|
40
|
+
// Local build in cwd (for development)
|
|
41
|
+
join(process.cwd(), 'relay-pty', 'target', 'release', 'relay-pty'),
|
|
42
|
+
// Installed globally
|
|
43
|
+
'/usr/local/bin/relay-pty',
|
|
44
|
+
// In node_modules (when installed as dependency)
|
|
45
|
+
join(process.cwd(), 'node_modules', 'agent-relay', 'bin', 'relay-pty'),
|
|
46
|
+
];
|
|
47
|
+
for (const candidate of candidates) {
|
|
48
|
+
if (existsSync(candidate)) {
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
// Active sessions
|
|
55
|
+
const sessions = new Map();
|
|
56
|
+
// Clean up old sessions periodically
|
|
57
|
+
// Use .unref() so this timer doesn't prevent the process from exiting
|
|
58
|
+
setInterval(() => {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
for (const [id, session] of sessions) {
|
|
61
|
+
if (now - session.createdAt.getTime() > 10 * 60 * 1000) {
|
|
62
|
+
if (session.process) {
|
|
63
|
+
try {
|
|
64
|
+
session.process.kill();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Process may already be dead
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
sessions.delete(id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}, 60000).unref();
|
|
74
|
+
/**
|
|
75
|
+
* Start CLI auth flow
|
|
76
|
+
*
|
|
77
|
+
* This function waits for the auth URL to be captured before returning,
|
|
78
|
+
* ensuring the caller can immediately open the OAuth popup.
|
|
79
|
+
*/
|
|
80
|
+
export async function startCLIAuth(provider, options = {}) {
|
|
81
|
+
const config = CLI_AUTH_CONFIG[provider];
|
|
82
|
+
if (!config) {
|
|
83
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
84
|
+
}
|
|
85
|
+
const sessionId = crypto.randomUUID();
|
|
86
|
+
const session = {
|
|
87
|
+
id: sessionId,
|
|
88
|
+
provider,
|
|
89
|
+
userId: options.userId,
|
|
90
|
+
status: 'starting',
|
|
91
|
+
output: '',
|
|
92
|
+
promptsHandled: [],
|
|
93
|
+
createdAt: new Date(),
|
|
94
|
+
};
|
|
95
|
+
sessions.set(sessionId, session);
|
|
96
|
+
logger.info('CLI auth session created', {
|
|
97
|
+
sessionId,
|
|
98
|
+
provider,
|
|
99
|
+
totalActiveSessions: sessions.size,
|
|
100
|
+
allSessionIds: Array.from(sessions.keys()),
|
|
101
|
+
});
|
|
102
|
+
// Check if already authenticated (credentials exist)
|
|
103
|
+
try {
|
|
104
|
+
const existingCreds = await extractCredentials(provider, config, options.userId);
|
|
105
|
+
if (existingCreds?.token) {
|
|
106
|
+
logger.info('Already authenticated - existing credentials found', { provider, sessionId });
|
|
107
|
+
session.status = 'success';
|
|
108
|
+
session.token = existingCreds.token;
|
|
109
|
+
session.refreshToken = existingCreds.refreshToken;
|
|
110
|
+
session.tokenExpiresAt = existingCreds.expiresAt;
|
|
111
|
+
return session;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// No existing credentials, proceed with auth flow
|
|
116
|
+
}
|
|
117
|
+
// Find relay-pty binary
|
|
118
|
+
const relayPtyPath = findRelayPtyBinary();
|
|
119
|
+
if (!relayPtyPath) {
|
|
120
|
+
session.status = 'error';
|
|
121
|
+
session.error = 'relay-pty binary not found. Build with: cd relay-pty && cargo build --release';
|
|
122
|
+
return session;
|
|
123
|
+
}
|
|
124
|
+
// Use device flow args if requested and supported
|
|
125
|
+
const args = options.useDeviceFlow && config.deviceFlowArgs
|
|
126
|
+
? config.deviceFlowArgs
|
|
127
|
+
: config.args;
|
|
128
|
+
logger.info('Starting CLI auth', {
|
|
129
|
+
provider,
|
|
130
|
+
sessionId,
|
|
131
|
+
useDeviceFlow: options.useDeviceFlow,
|
|
132
|
+
args,
|
|
133
|
+
});
|
|
134
|
+
const respondedPrompts = new Set();
|
|
135
|
+
// Create a promise that resolves when authUrl is captured or timeout
|
|
136
|
+
let resolveAuthUrl;
|
|
137
|
+
const authUrlPromise = new Promise((resolve) => {
|
|
138
|
+
resolveAuthUrl = resolve;
|
|
139
|
+
});
|
|
140
|
+
// Timeout for waiting for auth URL (shorter than the full OAuth timeout)
|
|
141
|
+
const AUTH_URL_WAIT_TIMEOUT = 15000; // 15 seconds to capture auth URL
|
|
142
|
+
const authUrlTimeout = setTimeout(() => {
|
|
143
|
+
logger.warn('Auth URL wait timeout, returning session without URL', { provider, sessionId });
|
|
144
|
+
resolveAuthUrl();
|
|
145
|
+
}, AUTH_URL_WAIT_TIMEOUT);
|
|
146
|
+
try {
|
|
147
|
+
// Get per-user environment if userId provided (for multi-user workspaces)
|
|
148
|
+
// This sets HOME to /data/users/{userId} so CLI stores credentials per-user
|
|
149
|
+
let userEnv = {};
|
|
150
|
+
if (options.userId) {
|
|
151
|
+
try {
|
|
152
|
+
const userDirService = getUserDirectoryService();
|
|
153
|
+
userEnv = userDirService.getUserEnvironment(options.userId);
|
|
154
|
+
logger.info('Using per-user environment for CLI auth', {
|
|
155
|
+
provider,
|
|
156
|
+
userId: options.userId,
|
|
157
|
+
home: userEnv.HOME,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
logger.warn('Failed to get user environment, using default', {
|
|
162
|
+
provider,
|
|
163
|
+
userId: options.userId,
|
|
164
|
+
error: err instanceof Error ? err.message : String(err),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Build relay-pty arguments
|
|
169
|
+
const relayArgs = [
|
|
170
|
+
'--name', `auth-${sessionId.substring(0, 8)}`,
|
|
171
|
+
'--rows', '30',
|
|
172
|
+
'--cols', '120',
|
|
173
|
+
'--log-level', 'error', // Suppress relay-pty logs
|
|
174
|
+
'--', config.command,
|
|
175
|
+
...args,
|
|
176
|
+
];
|
|
177
|
+
const proc = spawn(relayPtyPath, relayArgs, {
|
|
178
|
+
cwd: process.cwd(),
|
|
179
|
+
env: {
|
|
180
|
+
...process.env,
|
|
181
|
+
...userEnv, // Override HOME for per-user credential storage
|
|
182
|
+
NO_COLOR: '1',
|
|
183
|
+
TERM: 'xterm-256color',
|
|
184
|
+
// Don't set BROWSER - let CLI fail to open browser and fall back to manual paste mode
|
|
185
|
+
// Setting BROWSER: 'echo' caused CLI to think browser opened and wait for callback that never came
|
|
186
|
+
DISPLAY: '',
|
|
187
|
+
},
|
|
188
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
189
|
+
});
|
|
190
|
+
// Create wrapper for session management
|
|
191
|
+
const processWrapper = {
|
|
192
|
+
write: (data) => proc.stdin?.write(data),
|
|
193
|
+
kill: () => proc.kill(),
|
|
194
|
+
};
|
|
195
|
+
session.process = processWrapper;
|
|
196
|
+
// Timeout handler - give user plenty of time to complete OAuth flow
|
|
197
|
+
// 5 minutes should be enough for even slow OAuth flows
|
|
198
|
+
const OAUTH_COMPLETION_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
199
|
+
const timeout = setTimeout(() => {
|
|
200
|
+
if (session.status === 'starting' || session.status === 'waiting_auth') {
|
|
201
|
+
logger.warn('CLI auth timed out', { provider, sessionId, status: session.status });
|
|
202
|
+
proc.kill();
|
|
203
|
+
session.status = 'error';
|
|
204
|
+
session.error = 'Timeout waiting for auth completion (5 minutes). Please try again.';
|
|
205
|
+
}
|
|
206
|
+
}, config.waitTimeout + OAUTH_COMPLETION_TIMEOUT);
|
|
207
|
+
// Handle data from PTY output
|
|
208
|
+
const handleData = (data) => {
|
|
209
|
+
session.output += data;
|
|
210
|
+
const cleanText = stripAnsiCodes(data);
|
|
211
|
+
// Check for error patterns FIRST - if error detected, don't auto-respond to prompts
|
|
212
|
+
// This prevents us from auto-responding to "Press Enter to retry" in error messages
|
|
213
|
+
const matchedError = findMatchingError(data, config.errorPatterns);
|
|
214
|
+
if (matchedError && session.status !== 'error') {
|
|
215
|
+
logger.warn('Auth error detected', {
|
|
216
|
+
provider,
|
|
217
|
+
sessionId,
|
|
218
|
+
errorMessage: matchedError.message,
|
|
219
|
+
recoverable: matchedError.recoverable,
|
|
220
|
+
});
|
|
221
|
+
session.status = 'error';
|
|
222
|
+
session.error = matchedError.message;
|
|
223
|
+
session.errorHint = matchedError.hint;
|
|
224
|
+
session.recoverable = matchedError.recoverable;
|
|
225
|
+
}
|
|
226
|
+
// Don't auto-respond to prompts if we're in error state
|
|
227
|
+
// This prevents responding to "Press Enter to retry" after an error
|
|
228
|
+
if (session.status !== 'error') {
|
|
229
|
+
const matchingPrompt = findMatchingPrompt(data, config.prompts, respondedPrompts);
|
|
230
|
+
if (matchingPrompt) {
|
|
231
|
+
respondedPrompts.add(matchingPrompt.description);
|
|
232
|
+
session.promptsHandled.push(matchingPrompt.description);
|
|
233
|
+
logger.info('Auto-responding to prompt', { description: matchingPrompt.description });
|
|
234
|
+
const delay = matchingPrompt.delay ?? 100;
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
try {
|
|
237
|
+
proc.stdin?.write(matchingPrompt.response);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Process may have exited
|
|
241
|
+
}
|
|
242
|
+
}, delay);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Extract auth URL (only if not in error state and don't have URL yet)
|
|
246
|
+
const match = cleanText.match(config.urlPattern);
|
|
247
|
+
if (match && match[1] && !session.authUrl && session.status !== 'error') {
|
|
248
|
+
session.authUrl = match[1];
|
|
249
|
+
session.status = 'waiting_auth';
|
|
250
|
+
logger.info('Auth URL captured', { provider, url: session.authUrl });
|
|
251
|
+
// Signal that we have the auth URL
|
|
252
|
+
clearTimeout(authUrlTimeout);
|
|
253
|
+
resolveAuthUrl();
|
|
254
|
+
}
|
|
255
|
+
// Log all output after auth URL is captured (for debugging)
|
|
256
|
+
if (session.authUrl) {
|
|
257
|
+
const trimmedData = cleanText.trim();
|
|
258
|
+
if (trimmedData.length > 0) {
|
|
259
|
+
logger.info('PTY output after auth URL', {
|
|
260
|
+
provider,
|
|
261
|
+
sessionId,
|
|
262
|
+
output: trimmedData.substring(0, 500),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Check for success and try to extract credentials
|
|
267
|
+
// Don't override error status - if there was an error, keep it
|
|
268
|
+
if (session.status !== 'error' && matchesSuccessPattern(data, config.successPatterns)) {
|
|
269
|
+
session.status = 'success';
|
|
270
|
+
logger.info('Success pattern detected, attempting credential extraction', { provider });
|
|
271
|
+
// Try to extract credentials immediately (CLI may not exit after success)
|
|
272
|
+
// Use a small delay to let the CLI finish writing the file
|
|
273
|
+
setTimeout(async () => {
|
|
274
|
+
// Don't extract if status changed to error (e.g., error detected after success pattern)
|
|
275
|
+
if (session.status === 'error') {
|
|
276
|
+
logger.info('Skipping credential extraction - session is in error state', { provider });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const creds = await extractCredentials(provider, config, session.userId);
|
|
281
|
+
if (creds) {
|
|
282
|
+
session.token = creds.token;
|
|
283
|
+
session.refreshToken = creds.refreshToken;
|
|
284
|
+
session.tokenExpiresAt = creds.expiresAt;
|
|
285
|
+
logger.info('Credentials extracted successfully', { provider, hasRefreshToken: !!creds.refreshToken });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
logger.error('Failed to extract credentials on success', { error: String(err) });
|
|
290
|
+
}
|
|
291
|
+
}, 500);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
// Handle stdout (main PTY output)
|
|
295
|
+
proc.stdout?.on('data', (data) => {
|
|
296
|
+
handleData(data.toString());
|
|
297
|
+
});
|
|
298
|
+
// Handle stderr (relay-pty logs and some CLI output)
|
|
299
|
+
proc.stderr?.on('data', (data) => {
|
|
300
|
+
handleData(data.toString());
|
|
301
|
+
});
|
|
302
|
+
proc.on('exit', async (exitCode) => {
|
|
303
|
+
clearTimeout(timeout);
|
|
304
|
+
clearTimeout(authUrlTimeout);
|
|
305
|
+
// Clear process reference so submitAuthCode knows PTY is gone
|
|
306
|
+
session.process = undefined;
|
|
307
|
+
// Log full output for debugging PTY exit issues
|
|
308
|
+
const cleanOutput = stripAnsiCodes(session.output);
|
|
309
|
+
logger.info('CLI process exited', {
|
|
310
|
+
provider,
|
|
311
|
+
exitCode,
|
|
312
|
+
outputLength: session.output.length,
|
|
313
|
+
hasAuthUrl: !!session.authUrl,
|
|
314
|
+
sessionStatus: session.status,
|
|
315
|
+
promptsHandled: session.promptsHandled,
|
|
316
|
+
// Last 500 chars of output for debugging
|
|
317
|
+
outputTail: cleanOutput.slice(-500),
|
|
318
|
+
});
|
|
319
|
+
// Try to extract credentials (but don't override error status)
|
|
320
|
+
// CLI might exit cleanly (code 0) even after an OAuth error
|
|
321
|
+
if ((session.authUrl || exitCode === 0) && session.status !== 'error') {
|
|
322
|
+
try {
|
|
323
|
+
const creds = await extractCredentials(provider, config, session.userId);
|
|
324
|
+
if (creds) {
|
|
325
|
+
session.token = creds.token;
|
|
326
|
+
session.refreshToken = creds.refreshToken;
|
|
327
|
+
session.tokenExpiresAt = creds.expiresAt;
|
|
328
|
+
session.status = 'success';
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
logger.error('Failed to extract credentials', { error: String(err) });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!session.authUrl && !session.token && session.status !== 'error') {
|
|
336
|
+
session.status = 'error';
|
|
337
|
+
session.error = 'CLI exited without auth URL or credentials';
|
|
338
|
+
}
|
|
339
|
+
// Resolve in case we're still waiting
|
|
340
|
+
resolveAuthUrl();
|
|
341
|
+
});
|
|
342
|
+
proc.on('error', (err) => {
|
|
343
|
+
clearTimeout(timeout);
|
|
344
|
+
clearTimeout(authUrlTimeout);
|
|
345
|
+
session.status = 'error';
|
|
346
|
+
session.error = err.message;
|
|
347
|
+
logger.error('CLI process error', { error: err.message });
|
|
348
|
+
resolveAuthUrl();
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
session.status = 'error';
|
|
353
|
+
session.error = err instanceof Error ? err.message : 'Failed to spawn CLI';
|
|
354
|
+
logger.error('Failed to start CLI auth', { error: session.error });
|
|
355
|
+
clearTimeout(authUrlTimeout);
|
|
356
|
+
resolveAuthUrl();
|
|
357
|
+
}
|
|
358
|
+
// Wait for auth URL to be captured (or timeout)
|
|
359
|
+
await authUrlPromise;
|
|
360
|
+
return session;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Get auth session status
|
|
364
|
+
*/
|
|
365
|
+
export function getAuthSession(sessionId) {
|
|
366
|
+
return sessions.get(sessionId) || null;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Submit auth code to a waiting session
|
|
370
|
+
* This writes the code to the PTY process stdin
|
|
371
|
+
*
|
|
372
|
+
* @param sessionId - The auth session ID
|
|
373
|
+
* @param code - The OAuth authorization code
|
|
374
|
+
* @param state - Optional OAuth state parameter for CSRF validation (used by Codex)
|
|
375
|
+
* @returns Object with success status and optional error message
|
|
376
|
+
*/
|
|
377
|
+
export async function submitAuthCode(sessionId, code, state) {
|
|
378
|
+
// Log all active sessions for debugging
|
|
379
|
+
const activeSessionIds = Array.from(sessions.keys());
|
|
380
|
+
logger.info('submitAuthCode called', {
|
|
381
|
+
sessionId,
|
|
382
|
+
codeLength: code.length,
|
|
383
|
+
activeSessionCount: activeSessionIds.length,
|
|
384
|
+
activeSessionIds,
|
|
385
|
+
});
|
|
386
|
+
const session = sessions.get(sessionId);
|
|
387
|
+
if (!session) {
|
|
388
|
+
logger.warn('Auth code submission failed: session not found', {
|
|
389
|
+
sessionId,
|
|
390
|
+
activeSessionIds,
|
|
391
|
+
hint: 'Session may have been cleaned up or never created',
|
|
392
|
+
});
|
|
393
|
+
return { success: false, error: 'Session not found or expired', needsRestart: true };
|
|
394
|
+
}
|
|
395
|
+
logger.info('Session found for code submission', {
|
|
396
|
+
sessionId,
|
|
397
|
+
provider: session.provider,
|
|
398
|
+
status: session.status,
|
|
399
|
+
hasProcess: !!session.process,
|
|
400
|
+
hasAuthUrl: !!session.authUrl,
|
|
401
|
+
hasToken: !!session.token,
|
|
402
|
+
promptsHandled: session.promptsHandled,
|
|
403
|
+
createdAt: session.createdAt.toISOString(),
|
|
404
|
+
ageSeconds: Math.round((Date.now() - session.createdAt.getTime()) / 1000),
|
|
405
|
+
});
|
|
406
|
+
if (!session.process) {
|
|
407
|
+
logger.warn('Auth code submission failed: no PTY process', {
|
|
408
|
+
sessionId,
|
|
409
|
+
sessionStatus: session.status,
|
|
410
|
+
provider: session.provider,
|
|
411
|
+
outputLength: session.output?.length || 0,
|
|
412
|
+
outputTail: session.output ? stripAnsiCodes(session.output).slice(-500) : 'no output',
|
|
413
|
+
});
|
|
414
|
+
// Try to extract credentials as a fallback - maybe auth completed in browser
|
|
415
|
+
// But don't override error status
|
|
416
|
+
const config = CLI_AUTH_CONFIG[session.provider];
|
|
417
|
+
if (config && session.status !== 'error') {
|
|
418
|
+
try {
|
|
419
|
+
const creds = await extractCredentials(session.provider, config, session.userId);
|
|
420
|
+
// Re-check status after async operation (race condition protection)
|
|
421
|
+
// Use type assertion because TypeScript narrowing doesn't account for async race conditions
|
|
422
|
+
if (creds && session.status !== 'error') {
|
|
423
|
+
session.token = creds.token;
|
|
424
|
+
session.refreshToken = creds.refreshToken;
|
|
425
|
+
session.tokenExpiresAt = creds.expiresAt;
|
|
426
|
+
session.status = 'success';
|
|
427
|
+
logger.info('Credentials found despite PTY exit', { provider: session.provider });
|
|
428
|
+
return { success: true };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// No credentials found
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// For providers like Claude that need the code pasted into CLI,
|
|
436
|
+
// if the PTY is gone, user needs to restart the auth flow
|
|
437
|
+
return {
|
|
438
|
+
success: false,
|
|
439
|
+
error: 'The authentication session has ended. The CLI process exited before the code could be entered. Please click "Try Again" to restart.',
|
|
440
|
+
needsRestart: true,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
// Clean the code - trim whitespace and strip state parameter if present
|
|
445
|
+
// Claude OAuth codes come as "CODE#STATE" - we only need the code part
|
|
446
|
+
let cleanCode = code.trim();
|
|
447
|
+
if (cleanCode.includes('#')) {
|
|
448
|
+
const originalCode = cleanCode;
|
|
449
|
+
cleanCode = cleanCode.split('#')[0];
|
|
450
|
+
logger.info('Stripped state parameter from auth code', {
|
|
451
|
+
sessionId,
|
|
452
|
+
originalLength: originalCode.length,
|
|
453
|
+
cleanLength: cleanCode.length,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
// For Codex (openai), forward the callback to the CLI's localhost server
|
|
457
|
+
// instead of writing to PTY stdin. The CLI spawns a localhost server
|
|
458
|
+
// waiting for the OAuth callback.
|
|
459
|
+
if (session.provider === 'openai' && session.authUrl) {
|
|
460
|
+
// Extract the redirect port from the auth URL (usually 1455)
|
|
461
|
+
const redirectMatch = session.authUrl.match(/redirect_uri=http%3A%2F%2Flocalhost%3A(\d+)/);
|
|
462
|
+
const port = redirectMatch ? redirectMatch[1] : '1455';
|
|
463
|
+
logger.info('Forwarding OAuth callback to Codex CLI localhost server', {
|
|
464
|
+
sessionId,
|
|
465
|
+
port,
|
|
466
|
+
codeLength: cleanCode.length,
|
|
467
|
+
hasState: !!state,
|
|
468
|
+
});
|
|
469
|
+
try {
|
|
470
|
+
// Forward the callback to the CLI's localhost server
|
|
471
|
+
// Include state parameter for CSRF validation if provided
|
|
472
|
+
let callbackUrl = `http://localhost:${port}/auth/callback?code=${encodeURIComponent(cleanCode)}`;
|
|
473
|
+
if (state) {
|
|
474
|
+
callbackUrl += `&state=${encodeURIComponent(state)}`;
|
|
475
|
+
}
|
|
476
|
+
const response = await fetch(callbackUrl, {
|
|
477
|
+
method: 'GET',
|
|
478
|
+
signal: AbortSignal.timeout(5000),
|
|
479
|
+
});
|
|
480
|
+
if (response.ok) {
|
|
481
|
+
logger.info('OAuth callback forwarded successfully to Codex CLI', { sessionId, status: response.status });
|
|
482
|
+
// Start polling for credentials
|
|
483
|
+
const config = CLI_AUTH_CONFIG[session.provider];
|
|
484
|
+
if (config) {
|
|
485
|
+
pollForCredentials(session, config);
|
|
486
|
+
}
|
|
487
|
+
return { success: true };
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
// Try to get error details from response body
|
|
491
|
+
let errorBody = '';
|
|
492
|
+
try {
|
|
493
|
+
errorBody = await response.text();
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// Ignore
|
|
497
|
+
}
|
|
498
|
+
logger.warn('Codex CLI localhost server returned error', {
|
|
499
|
+
sessionId,
|
|
500
|
+
status: response.status,
|
|
501
|
+
statusText: response.statusText,
|
|
502
|
+
errorBody: errorBody.substring(0, 500), // Limit log size
|
|
503
|
+
callbackUrl: callbackUrl.replace(/code=[^&]+/, 'code=***'), // Redact code
|
|
504
|
+
});
|
|
505
|
+
// Fall through to PTY write as fallback
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
logger.warn('Failed to forward callback to Codex CLI localhost server', {
|
|
510
|
+
sessionId,
|
|
511
|
+
error: String(err),
|
|
512
|
+
});
|
|
513
|
+
// Fall through to PTY write as fallback
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
logger.info('Writing auth code to PTY', {
|
|
517
|
+
sessionId,
|
|
518
|
+
originalLength: code.length,
|
|
519
|
+
cleanLength: cleanCode.length,
|
|
520
|
+
codePreview: cleanCode.substring(0, 20) + '...',
|
|
521
|
+
});
|
|
522
|
+
// Write the auth code WITHOUT Enter first
|
|
523
|
+
// Claude CLI's Ink text input needs time to process the input
|
|
524
|
+
// before receiving Enter (tested: immediate Enter fails, delayed Enter works)
|
|
525
|
+
session.process.write(cleanCode);
|
|
526
|
+
logger.info('Auth code written, waiting before sending Enter...', { sessionId });
|
|
527
|
+
// Wait 1 second for CLI to process the typed input
|
|
528
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
529
|
+
// Now send Enter to submit
|
|
530
|
+
session.process.write('\r');
|
|
531
|
+
logger.info('Enter key sent', { sessionId });
|
|
532
|
+
// Start polling for credentials after code submission
|
|
533
|
+
// The CLI should write credentials shortly after receiving the code
|
|
534
|
+
const config = CLI_AUTH_CONFIG[session.provider];
|
|
535
|
+
if (config) {
|
|
536
|
+
pollForCredentials(session, config);
|
|
537
|
+
}
|
|
538
|
+
return { success: true };
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
logger.error('Failed to submit auth code', { sessionId, error: String(err) });
|
|
542
|
+
return {
|
|
543
|
+
success: false,
|
|
544
|
+
error: 'Failed to write to CLI process. The process may have exited. Please try again.',
|
|
545
|
+
needsRestart: true,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Poll for credentials file after auth code submission
|
|
551
|
+
* Some CLIs don't output success patterns, so we check the file directly
|
|
552
|
+
*/
|
|
553
|
+
async function pollForCredentials(session, config) {
|
|
554
|
+
const maxAttempts = 10;
|
|
555
|
+
const pollInterval = 1000; // 1 second
|
|
556
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
557
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
558
|
+
// Skip if session already has credentials or errored
|
|
559
|
+
if (session.token || session.status === 'error') {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const creds = await extractCredentials(session.provider, config, session.userId);
|
|
564
|
+
if (creds) {
|
|
565
|
+
// Double-check we're not in error state (race condition protection)
|
|
566
|
+
// Use type assertion because TypeScript narrowing doesn't account for async race conditions
|
|
567
|
+
if (session.status === 'error') {
|
|
568
|
+
logger.info('Credentials found but session is in error state, not overriding', {
|
|
569
|
+
provider: session.provider,
|
|
570
|
+
});
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
session.token = creds.token;
|
|
574
|
+
session.refreshToken = creds.refreshToken;
|
|
575
|
+
session.tokenExpiresAt = creds.expiresAt;
|
|
576
|
+
session.status = 'success';
|
|
577
|
+
logger.info('Credentials found via polling', {
|
|
578
|
+
provider: session.provider,
|
|
579
|
+
attempt: i + 1,
|
|
580
|
+
hasRefreshToken: !!creds.refreshToken,
|
|
581
|
+
});
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
// File doesn't exist yet, continue polling
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
logger.warn('Credential polling completed without finding credentials', {
|
|
590
|
+
provider: session.provider,
|
|
591
|
+
sessionId: session.id,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Complete auth session by polling for credentials
|
|
596
|
+
* Called when user indicates they've completed auth in browser
|
|
597
|
+
*/
|
|
598
|
+
export async function completeAuthSession(sessionId) {
|
|
599
|
+
const session = sessions.get(sessionId);
|
|
600
|
+
if (!session) {
|
|
601
|
+
return { success: false, error: 'Session not found or expired' };
|
|
602
|
+
}
|
|
603
|
+
// Already have credentials
|
|
604
|
+
if (session.token) {
|
|
605
|
+
return { success: true, token: session.token };
|
|
606
|
+
}
|
|
607
|
+
const config = CLI_AUTH_CONFIG[session.provider];
|
|
608
|
+
if (!config) {
|
|
609
|
+
return { success: false, error: 'Unknown provider' };
|
|
610
|
+
}
|
|
611
|
+
// Poll for credentials (user just completed auth in browser)
|
|
612
|
+
const maxAttempts = 15;
|
|
613
|
+
const pollInterval = 1000;
|
|
614
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
615
|
+
// Check if session went into error state
|
|
616
|
+
if (session.status === 'error') {
|
|
617
|
+
return { success: false, error: session.error || 'Authentication failed' };
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
const creds = await extractCredentials(session.provider, config, session.userId);
|
|
621
|
+
if (creds) {
|
|
622
|
+
// Double-check we're not in error state (race condition protection)
|
|
623
|
+
// Use type assertion because TypeScript narrowing doesn't account for async race conditions
|
|
624
|
+
if (session.status === 'error') {
|
|
625
|
+
return { success: false, error: session.error || 'Authentication failed' };
|
|
626
|
+
}
|
|
627
|
+
session.token = creds.token;
|
|
628
|
+
session.refreshToken = creds.refreshToken;
|
|
629
|
+
session.tokenExpiresAt = creds.expiresAt;
|
|
630
|
+
session.status = 'success';
|
|
631
|
+
logger.info('Credentials found via complete polling', {
|
|
632
|
+
provider: session.provider,
|
|
633
|
+
attempt: i + 1,
|
|
634
|
+
});
|
|
635
|
+
return { success: true, token: creds.token };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// File doesn't exist yet
|
|
640
|
+
}
|
|
641
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
success: false,
|
|
645
|
+
error: 'Credentials not found. Please ensure you completed authentication in the browser.',
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Cancel auth session
|
|
650
|
+
*/
|
|
651
|
+
export function cancelAuthSession(sessionId) {
|
|
652
|
+
const session = sessions.get(sessionId);
|
|
653
|
+
if (!session)
|
|
654
|
+
return false;
|
|
655
|
+
if (session.process) {
|
|
656
|
+
try {
|
|
657
|
+
session.process.kill();
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
// Already dead
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
sessions.delete(sessionId);
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
function resolveCredentialPath(provider, config, userId) {
|
|
667
|
+
if (!config.credentialPath)
|
|
668
|
+
return null;
|
|
669
|
+
if (!userId) {
|
|
670
|
+
return config.credentialPath.replace('~', os.homedir());
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
const userDirService = getUserDirectoryService();
|
|
674
|
+
const userHome = userDirService.getUserHome(userId);
|
|
675
|
+
return config.credentialPath.replace('~', userHome);
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
logger.warn('Failed to resolve per-user credential path, using default', {
|
|
679
|
+
provider,
|
|
680
|
+
userId,
|
|
681
|
+
error: err instanceof Error ? err.message : String(err),
|
|
682
|
+
});
|
|
683
|
+
return config.credentialPath.replace('~', os.homedir());
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Extract credentials from CLI credential file
|
|
688
|
+
*/
|
|
689
|
+
async function extractCredentials(provider, config, userId) {
|
|
690
|
+
const credPath = resolveCredentialPath(provider, config, userId);
|
|
691
|
+
if (!credPath)
|
|
692
|
+
return null;
|
|
693
|
+
try {
|
|
694
|
+
const content = await fs.readFile(credPath, 'utf8');
|
|
695
|
+
const creds = JSON.parse(content);
|
|
696
|
+
// Extract token based on provider
|
|
697
|
+
if (provider === 'anthropic') {
|
|
698
|
+
// Claude stores OAuth in: { claudeAiOauth: { accessToken: "...", refreshToken: "...", expiresAt: ... } }
|
|
699
|
+
if (creds.claudeAiOauth?.accessToken) {
|
|
700
|
+
return {
|
|
701
|
+
token: creds.claudeAiOauth.accessToken,
|
|
702
|
+
refreshToken: creds.claudeAiOauth.refreshToken,
|
|
703
|
+
expiresAt: creds.claudeAiOauth.expiresAt ? new Date(creds.claudeAiOauth.expiresAt) : undefined,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
// Fallback to legacy formats
|
|
707
|
+
const token = creds.oauth_token || creds.access_token || creds.api_key;
|
|
708
|
+
return token ? { token } : null;
|
|
709
|
+
}
|
|
710
|
+
else if (provider === 'openai') {
|
|
711
|
+
// Codex stores OAuth in: { tokens: { access_token: "...", refresh_token: "...", ... } }
|
|
712
|
+
if (creds.tokens?.access_token) {
|
|
713
|
+
return {
|
|
714
|
+
token: creds.tokens.access_token,
|
|
715
|
+
refreshToken: creds.tokens.refresh_token,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
// Fallback: API key or legacy formats
|
|
719
|
+
const token = creds.OPENAI_API_KEY || creds.token || creds.access_token || creds.api_key;
|
|
720
|
+
return token ? { token } : null;
|
|
721
|
+
}
|
|
722
|
+
else if (provider === 'opencode') {
|
|
723
|
+
// OpenCode stores multiple providers: { opencode: {...}, anthropic: {...}, openai: {...}, google: {...} }
|
|
724
|
+
// Check for any valid credential - prefer OpenCode Zen, then Anthropic
|
|
725
|
+
if (creds.opencode?.key) {
|
|
726
|
+
return { token: creds.opencode.key };
|
|
727
|
+
}
|
|
728
|
+
if (creds.anthropic?.access) {
|
|
729
|
+
return {
|
|
730
|
+
token: creds.anthropic.access,
|
|
731
|
+
refreshToken: creds.anthropic.refresh,
|
|
732
|
+
expiresAt: creds.anthropic.expires ? new Date(creds.anthropic.expires) : undefined,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (creds.openai?.access) {
|
|
736
|
+
return {
|
|
737
|
+
token: creds.openai.access,
|
|
738
|
+
refreshToken: creds.openai.refresh,
|
|
739
|
+
expiresAt: creds.openai.expires ? new Date(creds.openai.expires) : undefined,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
if (creds.google?.key) {
|
|
743
|
+
return { token: creds.google.key };
|
|
744
|
+
}
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
else if (provider === 'cursor') {
|
|
748
|
+
// Cursor stores credentials in various formats - try common patterns
|
|
749
|
+
// { accessToken: "...", refreshToken: "..." } or { token: "..." } or nested
|
|
750
|
+
if (creds.accessToken) {
|
|
751
|
+
return {
|
|
752
|
+
token: creds.accessToken,
|
|
753
|
+
refreshToken: creds.refreshToken,
|
|
754
|
+
expiresAt: creds.expiresAt ? new Date(creds.expiresAt) : undefined,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
if (creds.auth?.accessToken) {
|
|
758
|
+
return {
|
|
759
|
+
token: creds.auth.accessToken,
|
|
760
|
+
refreshToken: creds.auth.refreshToken,
|
|
761
|
+
expiresAt: creds.auth.expiresAt ? new Date(creds.auth.expiresAt) : undefined,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
// Fallback to generic token fields
|
|
765
|
+
const token = creds.token || creds.access_token || creds.api_key;
|
|
766
|
+
return token ? { token } : null;
|
|
767
|
+
}
|
|
768
|
+
const token = creds.token || creds.access_token || creds.api_key;
|
|
769
|
+
return token ? { token } : null;
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Check if a provider is authenticated (credentials exist)
|
|
777
|
+
* Used by the auth check endpoint for SSH tunnel flow
|
|
778
|
+
*/
|
|
779
|
+
export async function checkProviderAuth(provider, userId) {
|
|
780
|
+
const config = CLI_AUTH_CONFIG[provider];
|
|
781
|
+
if (!config) {
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
const creds = await extractCredentials(provider, config, userId);
|
|
786
|
+
return !!creds?.token;
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
//# sourceMappingURL=cli-auth.js.map
|