@lindblad/complai-mcp 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.
@@ -0,0 +1,274 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ const CREDENTIALS_DIR = join(homedir(), '.complai');
6
+ const CREDENTIALS_FILE = join(CREDENTIALS_DIR, '.credentials.enc');
7
+ const ENCRYPTION_KEY_FILE = join(CREDENTIALS_DIR, '.key');
8
+ /**
9
+ * Device Authorization Manager
10
+ * Handles the device code flow and token storage
11
+ */
12
+ export class DeviceAuthManager {
13
+ config;
14
+ cachedAccessToken = null;
15
+ constructor(config) {
16
+ this.config = {
17
+ ...config,
18
+ scope: config.scope || 'openid profile email offline_access',
19
+ };
20
+ }
21
+ /**
22
+ * Get a valid access token, prompting for login if needed
23
+ */
24
+ async getAccessToken() {
25
+ // Check cached token
26
+ if (this.cachedAccessToken && this.cachedAccessToken.expiresAt > Date.now() + 60000) {
27
+ return this.cachedAccessToken.token;
28
+ }
29
+ // Try to use stored refresh token
30
+ const stored = this.loadStoredCredentials();
31
+ if (stored && stored.refreshToken) {
32
+ try {
33
+ const tokens = await this.refreshAccessToken(stored.refreshToken);
34
+ this.cacheToken(tokens);
35
+ // Update stored refresh token if a new one was issued
36
+ if (tokens.refresh_token) {
37
+ this.storeCredentials(tokens.refresh_token);
38
+ }
39
+ return tokens.access_token;
40
+ }
41
+ catch (error) {
42
+ // Refresh token expired or invalid - need to re-authenticate
43
+ console.error('Stored credentials expired. Please re-authenticate.');
44
+ }
45
+ }
46
+ // No valid credentials - need device authorization
47
+ throw new AuthenticationRequiredError('Authentication required. Run the login command first.', this.config.domain, this.config.clientId);
48
+ }
49
+ /**
50
+ * Initiate device authorization flow
51
+ * Returns user instructions for completing authentication
52
+ */
53
+ async initiateDeviceAuthorization() {
54
+ const response = await fetch(`https://${this.config.domain}/oauth/device/code`, {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
57
+ body: new URLSearchParams({
58
+ client_id: this.config.clientId,
59
+ scope: this.config.scope,
60
+ audience: this.config.audience,
61
+ }),
62
+ });
63
+ if (!response.ok) {
64
+ const error = await response.text();
65
+ throw new Error(`Failed to initiate device authorization: ${error}`);
66
+ }
67
+ const deviceCode = (await response.json());
68
+ return {
69
+ userCode: deviceCode.user_code,
70
+ verificationUri: deviceCode.verification_uri,
71
+ verificationUriComplete: deviceCode.verification_uri_complete,
72
+ expiresIn: deviceCode.expires_in,
73
+ pollForToken: () => this.pollForToken(deviceCode),
74
+ };
75
+ }
76
+ /**
77
+ * Poll Auth0 for token after user completes authentication
78
+ */
79
+ async pollForToken(deviceCode) {
80
+ const startTime = Date.now();
81
+ const expiresAt = startTime + deviceCode.expires_in * 1000;
82
+ const interval = (deviceCode.interval || 5) * 1000;
83
+ while (Date.now() < expiresAt) {
84
+ await this.sleep(interval);
85
+ const response = await fetch(`https://${this.config.domain}/oauth/token`, {
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
88
+ body: new URLSearchParams({
89
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
90
+ device_code: deviceCode.device_code,
91
+ client_id: this.config.clientId,
92
+ }),
93
+ });
94
+ if (response.ok) {
95
+ const tokens = (await response.json());
96
+ // Store refresh token for future use
97
+ if (tokens.refresh_token) {
98
+ this.storeCredentials(tokens.refresh_token);
99
+ }
100
+ this.cacheToken(tokens);
101
+ return tokens;
102
+ }
103
+ const error = await response.json();
104
+ if (error.error === 'authorization_pending') {
105
+ // User hasn't completed authorization yet - keep polling
106
+ continue;
107
+ }
108
+ else if (error.error === 'slow_down') {
109
+ // Need to slow down polling
110
+ await this.sleep(interval);
111
+ continue;
112
+ }
113
+ else if (error.error === 'expired_token') {
114
+ throw new Error('Device code expired. Please try again.');
115
+ }
116
+ else if (error.error === 'access_denied') {
117
+ throw new Error('Authorization was denied by the user.');
118
+ }
119
+ else {
120
+ throw new Error(`Token error: ${error.error_description || error.error}`);
121
+ }
122
+ }
123
+ throw new Error('Device authorization timed out. Please try again.');
124
+ }
125
+ /**
126
+ * Refresh the access token using a refresh token
127
+ */
128
+ async refreshAccessToken(refreshToken) {
129
+ const response = await fetch(`https://${this.config.domain}/oauth/token`, {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
132
+ body: new URLSearchParams({
133
+ grant_type: 'refresh_token',
134
+ client_id: this.config.clientId,
135
+ refresh_token: refreshToken,
136
+ }),
137
+ });
138
+ if (!response.ok) {
139
+ const error = await response.text();
140
+ throw new Error(`Failed to refresh token: ${error}`);
141
+ }
142
+ return (await response.json());
143
+ }
144
+ /**
145
+ * Cache access token in memory
146
+ */
147
+ cacheToken(tokens) {
148
+ this.cachedAccessToken = {
149
+ token: tokens.access_token,
150
+ expiresAt: Date.now() + tokens.expires_in * 1000,
151
+ };
152
+ }
153
+ /**
154
+ * Store refresh token encrypted on disk
155
+ */
156
+ storeCredentials(refreshToken) {
157
+ const credentials = {
158
+ refreshToken,
159
+ domain: this.config.domain,
160
+ clientId: this.config.clientId,
161
+ audience: this.config.audience,
162
+ };
163
+ const encrypted = this.encrypt(JSON.stringify(credentials));
164
+ // Ensure directory exists
165
+ if (!existsSync(CREDENTIALS_DIR)) {
166
+ mkdirSync(CREDENTIALS_DIR, { mode: 0o700 });
167
+ }
168
+ writeFileSync(CREDENTIALS_FILE, encrypted, { mode: 0o600 });
169
+ }
170
+ /**
171
+ * Load stored credentials from disk
172
+ */
173
+ loadStoredCredentials() {
174
+ if (!existsSync(CREDENTIALS_FILE)) {
175
+ return null;
176
+ }
177
+ try {
178
+ const encrypted = readFileSync(CREDENTIALS_FILE, 'utf-8');
179
+ const decrypted = this.decrypt(encrypted);
180
+ const credentials = JSON.parse(decrypted);
181
+ // Verify credentials match current config
182
+ if (credentials.domain !== this.config.domain ||
183
+ credentials.clientId !== this.config.clientId ||
184
+ credentials.audience !== this.config.audience) {
185
+ // Config changed - credentials no longer valid
186
+ return null;
187
+ }
188
+ return credentials;
189
+ }
190
+ catch {
191
+ return null;
192
+ }
193
+ }
194
+ /**
195
+ * Clear stored credentials (logout)
196
+ */
197
+ clearCredentials() {
198
+ this.cachedAccessToken = null;
199
+ if (existsSync(CREDENTIALS_FILE)) {
200
+ writeFileSync(CREDENTIALS_FILE, '', { mode: 0o600 });
201
+ }
202
+ }
203
+ /**
204
+ * Check if user is authenticated
205
+ */
206
+ isAuthenticated() {
207
+ console.error(`[DEBUG] Checking credentials at ${CREDENTIALS_FILE}`);
208
+ const stored = this.loadStoredCredentials();
209
+ console.error(`[DEBUG] Stored credentials: ${stored ? 'found' : 'not found'}`);
210
+ return stored !== null && !!stored.refreshToken;
211
+ }
212
+ /**
213
+ * Get or create encryption key
214
+ */
215
+ getEncryptionKey() {
216
+ if (!existsSync(CREDENTIALS_DIR)) {
217
+ mkdirSync(CREDENTIALS_DIR, { mode: 0o700 });
218
+ }
219
+ if (existsSync(ENCRYPTION_KEY_FILE)) {
220
+ return Buffer.from(readFileSync(ENCRYPTION_KEY_FILE, 'utf-8'), 'hex');
221
+ }
222
+ // Generate new key
223
+ const key = randomBytes(32);
224
+ writeFileSync(ENCRYPTION_KEY_FILE, key.toString('hex'), { mode: 0o600 });
225
+ return key;
226
+ }
227
+ /**
228
+ * Encrypt data using AES-256-GCM
229
+ */
230
+ encrypt(data) {
231
+ const key = this.getEncryptionKey();
232
+ const iv = randomBytes(16);
233
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
234
+ let encrypted = cipher.update(data, 'utf8', 'hex');
235
+ encrypted += cipher.final('hex');
236
+ const authTag = cipher.getAuthTag();
237
+ return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
238
+ }
239
+ /**
240
+ * Decrypt data using AES-256-GCM
241
+ */
242
+ decrypt(data) {
243
+ const key = this.getEncryptionKey();
244
+ const parts = data.split(':');
245
+ if (parts.length !== 3) {
246
+ throw new Error('Invalid encrypted data format');
247
+ }
248
+ const iv = Buffer.from(parts[0], 'hex');
249
+ const authTag = Buffer.from(parts[1], 'hex');
250
+ const encrypted = parts[2];
251
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
252
+ decipher.setAuthTag(authTag);
253
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
254
+ decrypted += decipher.final('utf8');
255
+ return decrypted;
256
+ }
257
+ sleep(ms) {
258
+ return new Promise(resolve => setTimeout(resolve, ms));
259
+ }
260
+ }
261
+ /**
262
+ * Error thrown when authentication is required
263
+ */
264
+ export class AuthenticationRequiredError extends Error {
265
+ domain;
266
+ clientId;
267
+ constructor(message, domain, clientId) {
268
+ super(message);
269
+ this.domain = domain;
270
+ this.clientId = clientId;
271
+ this.name = 'AuthenticationRequiredError';
272
+ }
273
+ }
274
+ //# sourceMappingURL=device-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-auth.js","sourceRoot":"","sources":["../../src/auth/device-auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ export interface PkceAuthConfig {
2
+ domain: string;
3
+ clientId: string;
4
+ audience: string;
5
+ organizationId?: string;
6
+ scope?: string;
7
+ }
8
+ /**
9
+ * PKCE Authentication Manager
10
+ * Uses Authorization Code Flow with PKCE for secure browser-based login
11
+ */
12
+ export declare class PkceAuthManager {
13
+ private config;
14
+ private cachedToken;
15
+ constructor(config: PkceAuthConfig);
16
+ /**
17
+ * Get id_token for API auth (like webapp pattern)
18
+ */
19
+ getAccessToken(): Promise<string>;
20
+ /**
21
+ * Check if user is authenticated (has valid or refreshable tokens)
22
+ */
23
+ isAuthenticated(): boolean;
24
+ /**
25
+ * Perform interactive PKCE login
26
+ * Opens browser, starts local server for callback, exchanges code for tokens
27
+ */
28
+ login(): Promise<void>;
29
+ /**
30
+ * Perform interactive PKCE login with a specific organization
31
+ * This ensures the token has org_id claim for org-scoped access
32
+ */
33
+ loginWithOrganization(organizationId: string): Promise<void>;
34
+ /**
35
+ * Clear stored credentials
36
+ */
37
+ logout(): void;
38
+ /**
39
+ * Generate PKCE verifier and challenge
40
+ */
41
+ private generatePkce;
42
+ /**
43
+ * Find an available port for the callback server
44
+ */
45
+ private findAvailablePort;
46
+ /**
47
+ * Start a temporary HTTP server to receive the OAuth callback
48
+ */
49
+ private startCallbackServer;
50
+ /**
51
+ * Exchange authorization code for tokens
52
+ */
53
+ private exchangeCodeForTokens;
54
+ /**
55
+ * Refresh access token using refresh token
56
+ */
57
+ private refreshAccessToken;
58
+ /**
59
+ * Cache token in memory and store to disk
60
+ */
61
+ private cacheAndStore;
62
+ /**
63
+ * Load stored tokens from disk
64
+ */
65
+ private loadStoredTokens;
66
+ }
67
+ //# sourceMappingURL=pkce-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce-auth.d.ts","sourceRoot":"","sources":["../../src/auth/pkce-auth.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAmBD;;;GAGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,WAAW,CAAqD;gBAE5D,MAAM,EAAE,cAAc;IAOlC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAiCvC;;OAEG;IACH,eAAe,IAAI,OAAO;IAQ1B;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA+B5B;;;OAGG;IACG,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2BlE;;OAEG;IACH,MAAM,IAAI,IAAI;IAOd;;OAEG;IACH,OAAO,CAAC,YAAY;IAMpB;;OAEG;YACW,iBAAiB;IAe/B;;OAEG;YACW,mBAAmB;IA2EjC;;OAEG;YACW,qBAAqB;IAyBnC;;OAEG;YACW,kBAAkB;IAmBhC;;OAEG;IACH,OAAO,CAAC,aAAa;IAyBrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAqBzB"}
@@ -0,0 +1,311 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomBytes, createHash } from 'node:crypto';
3
+ import { URL } from 'node:url';
4
+ import open from 'open';
5
+ import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ const CREDENTIALS_DIR = join(homedir(), '.complai');
9
+ const TOKEN_FILE = join(CREDENTIALS_DIR, 'tokens.json');
10
+ /**
11
+ * PKCE Authentication Manager
12
+ * Uses Authorization Code Flow with PKCE for secure browser-based login
13
+ */
14
+ export class PkceAuthManager {
15
+ config;
16
+ cachedToken = null;
17
+ constructor(config) {
18
+ this.config = {
19
+ ...config,
20
+ scope: config.scope || 'openid profile email offline_access',
21
+ };
22
+ }
23
+ /**
24
+ * Get id_token for API auth (like webapp pattern)
25
+ */
26
+ async getAccessToken() {
27
+ // Check memory cache
28
+ if (this.cachedToken && this.cachedToken.expiresAt > Date.now() + 60000) {
29
+ return this.cachedToken.token;
30
+ }
31
+ // Check stored tokens
32
+ const stored = this.loadStoredTokens();
33
+ if (stored) {
34
+ // Use id_token like webapp does (aud = client_id, accepted by API)
35
+ const token = stored.idToken || stored.accessToken;
36
+ // Check if token is still valid
37
+ if (stored.expiresAt > Date.now() + 60000) {
38
+ this.cachedToken = { token, expiresAt: stored.expiresAt };
39
+ return token;
40
+ }
41
+ // Try to refresh
42
+ if (stored.refreshToken) {
43
+ try {
44
+ const tokens = await this.refreshAccessToken(stored.refreshToken);
45
+ this.cacheAndStore(tokens);
46
+ return tokens.id_token || tokens.access_token;
47
+ }
48
+ catch (error) {
49
+ console.error('[PKCE] Refresh failed, need to re-authenticate');
50
+ }
51
+ }
52
+ }
53
+ throw new Error('Not authenticated. Please run login first.');
54
+ }
55
+ /**
56
+ * Check if user is authenticated (has valid or refreshable tokens)
57
+ */
58
+ isAuthenticated() {
59
+ const stored = this.loadStoredTokens();
60
+ if (!stored)
61
+ return false;
62
+ // Either access token is valid, or we have a refresh token
63
+ return stored.expiresAt > Date.now() || !!stored.refreshToken;
64
+ }
65
+ /**
66
+ * Perform interactive PKCE login
67
+ * Opens browser, starts local server for callback, exchanges code for tokens
68
+ */
69
+ async login() {
70
+ const { verifier, challenge } = this.generatePkce();
71
+ const state = randomBytes(16).toString('hex');
72
+ const port = 8374; // Fixed port for Auth0 callback URL matching
73
+ const redirectUri = `http://localhost:${port}/callback`;
74
+ // Build authorization URL (no audience - use id_token like webapp)
75
+ const authUrl = new URL(`https://${this.config.domain}/authorize`);
76
+ authUrl.searchParams.set('response_type', 'code');
77
+ authUrl.searchParams.set('client_id', this.config.clientId);
78
+ authUrl.searchParams.set('redirect_uri', redirectUri);
79
+ authUrl.searchParams.set('scope', this.config.scope);
80
+ authUrl.searchParams.set('state', state);
81
+ authUrl.searchParams.set('code_challenge', challenge);
82
+ authUrl.searchParams.set('code_challenge_method', 'S256');
83
+ // Include organization if configured (required for org_id in token)
84
+ if (this.config.organizationId) {
85
+ authUrl.searchParams.set('organization', this.config.organizationId);
86
+ }
87
+ // Start local server to receive callback and open browser
88
+ const code = await this.startCallbackServer(port, state, authUrl.toString());
89
+ // Exchange code for tokens
90
+ const tokens = await this.exchangeCodeForTokens(code, verifier, redirectUri);
91
+ this.cacheAndStore(tokens);
92
+ console.error('✓ Login successful!');
93
+ }
94
+ /**
95
+ * Perform interactive PKCE login with a specific organization
96
+ * This ensures the token has org_id claim for org-scoped access
97
+ */
98
+ async loginWithOrganization(organizationId) {
99
+ const { verifier, challenge } = this.generatePkce();
100
+ const state = randomBytes(16).toString('hex');
101
+ const port = 8374; // Fixed port for Auth0 callback URL matching
102
+ const redirectUri = `http://localhost:${port}/callback`;
103
+ // Build authorization URL with organization
104
+ const authUrl = new URL(`https://${this.config.domain}/authorize`);
105
+ authUrl.searchParams.set('response_type', 'code');
106
+ authUrl.searchParams.set('client_id', this.config.clientId);
107
+ authUrl.searchParams.set('redirect_uri', redirectUri);
108
+ authUrl.searchParams.set('scope', this.config.scope);
109
+ authUrl.searchParams.set('state', state);
110
+ authUrl.searchParams.set('code_challenge', challenge);
111
+ authUrl.searchParams.set('code_challenge_method', 'S256');
112
+ authUrl.searchParams.set('organization', organizationId); // Always include org
113
+ // Start local server to receive callback and open browser
114
+ const code = await this.startCallbackServer(port, state, authUrl.toString());
115
+ // Exchange code for tokens
116
+ const tokens = await this.exchangeCodeForTokens(code, verifier, redirectUri);
117
+ this.cacheAndStore(tokens);
118
+ console.error(`✓ Login successful for organization ${organizationId}!`);
119
+ }
120
+ /**
121
+ * Clear stored credentials
122
+ */
123
+ logout() {
124
+ this.cachedToken = null;
125
+ if (existsSync(TOKEN_FILE)) {
126
+ writeFileSync(TOKEN_FILE, '', { mode: 0o600 });
127
+ }
128
+ }
129
+ /**
130
+ * Generate PKCE verifier and challenge
131
+ */
132
+ generatePkce() {
133
+ const verifier = randomBytes(32).toString('base64url');
134
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
135
+ return { verifier, challenge };
136
+ }
137
+ /**
138
+ * Find an available port for the callback server
139
+ */
140
+ async findAvailablePort() {
141
+ return new Promise((resolve, reject) => {
142
+ const server = createServer();
143
+ server.listen(0, () => {
144
+ const address = server.address();
145
+ if (address && typeof address === 'object') {
146
+ const port = address.port;
147
+ server.close(() => resolve(port));
148
+ }
149
+ else {
150
+ reject(new Error('Could not find available port'));
151
+ }
152
+ });
153
+ });
154
+ }
155
+ /**
156
+ * Start a temporary HTTP server to receive the OAuth callback
157
+ */
158
+ async startCallbackServer(port, expectedState, authUrl) {
159
+ return new Promise((resolve, reject) => {
160
+ const server = createServer((req, res) => {
161
+ const url = new URL(req.url || '/', `http://localhost:${port}`);
162
+ if (url.pathname === '/callback') {
163
+ const code = url.searchParams.get('code');
164
+ const state = url.searchParams.get('state');
165
+ const error = url.searchParams.get('error');
166
+ if (error) {
167
+ const errorDesc = url.searchParams.get('error_description') || 'No description';
168
+ console.error(`Auth0 error: ${error} - ${errorDesc}`);
169
+ res.writeHead(400, { 'Content-Type': 'text/html' });
170
+ res.end(`<html><body><h1>Login Failed</h1><p>${error}: ${errorDesc}</p><p>You can close this window.</p></body></html>`);
171
+ server.close();
172
+ reject(new Error(`Auth error: ${error} - ${errorDesc}`));
173
+ return;
174
+ }
175
+ if (state !== expectedState) {
176
+ res.writeHead(400, { 'Content-Type': 'text/html' });
177
+ res.end('<html><body><h1>Invalid State</h1><p>You can close this window.</p></body></html>');
178
+ server.close();
179
+ reject(new Error('State mismatch - possible CSRF attack'));
180
+ return;
181
+ }
182
+ if (!code) {
183
+ res.writeHead(400, { 'Content-Type': 'text/html' });
184
+ res.end('<html><body><h1>No Code</h1><p>You can close this window.</p></body></html>');
185
+ server.close();
186
+ reject(new Error('No authorization code received'));
187
+ return;
188
+ }
189
+ res.writeHead(200, { 'Content-Type': 'text/html' });
190
+ res.end(`
191
+ <html>
192
+ <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f0f0f0;">
193
+ <div style="background: white; padding: 40px; border-radius: 8px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
194
+ <h1 style="color: #22c55e;">✓ Login Successful!</h1>
195
+ <p>You can close this window and return to your terminal.</p>
196
+ </div>
197
+ </body>
198
+ </html>
199
+ `);
200
+ server.close();
201
+ resolve(code);
202
+ }
203
+ else {
204
+ res.writeHead(404);
205
+ res.end('Not found');
206
+ }
207
+ });
208
+ server.listen(port, async () => {
209
+ console.error(`\nOpening browser for login...`);
210
+ try {
211
+ await open(authUrl);
212
+ }
213
+ catch {
214
+ console.error('Could not open browser automatically.');
215
+ console.error(`Please open this URL manually:\n${authUrl}\n`);
216
+ }
217
+ });
218
+ // Timeout after 5 minutes
219
+ setTimeout(() => {
220
+ server.close();
221
+ reject(new Error('Login timed out after 5 minutes'));
222
+ }, 5 * 60 * 1000);
223
+ });
224
+ }
225
+ /**
226
+ * Exchange authorization code for tokens
227
+ */
228
+ async exchangeCodeForTokens(code, verifier, redirectUri) {
229
+ const response = await fetch(`https://${this.config.domain}/oauth/token`, {
230
+ method: 'POST',
231
+ headers: { 'Content-Type': 'application/json' },
232
+ body: JSON.stringify({
233
+ grant_type: 'authorization_code',
234
+ client_id: this.config.clientId,
235
+ code,
236
+ code_verifier: verifier,
237
+ redirect_uri: redirectUri,
238
+ }),
239
+ });
240
+ if (!response.ok) {
241
+ const error = await response.text();
242
+ throw new Error(`Token exchange failed: ${error}`);
243
+ }
244
+ return (await response.json());
245
+ }
246
+ /**
247
+ * Refresh access token using refresh token
248
+ */
249
+ async refreshAccessToken(refreshToken) {
250
+ const response = await fetch(`https://${this.config.domain}/oauth/token`, {
251
+ method: 'POST',
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify({
254
+ grant_type: 'refresh_token',
255
+ client_id: this.config.clientId,
256
+ refresh_token: refreshToken,
257
+ }),
258
+ });
259
+ if (!response.ok) {
260
+ const error = await response.text();
261
+ throw new Error(`Token refresh failed: ${error}`);
262
+ }
263
+ return (await response.json());
264
+ }
265
+ /**
266
+ * Cache token in memory and store to disk
267
+ */
268
+ cacheAndStore(tokens) {
269
+ const expiresAt = Date.now() + tokens.expires_in * 1000;
270
+ // Use id_token like webapp does
271
+ this.cachedToken = {
272
+ token: tokens.id_token || tokens.access_token,
273
+ expiresAt,
274
+ };
275
+ const stored = {
276
+ accessToken: tokens.access_token,
277
+ idToken: tokens.id_token,
278
+ refreshToken: tokens.refresh_token,
279
+ expiresAt,
280
+ domain: this.config.domain,
281
+ clientId: this.config.clientId,
282
+ };
283
+ if (!existsSync(CREDENTIALS_DIR)) {
284
+ mkdirSync(CREDENTIALS_DIR, { mode: 0o700 });
285
+ }
286
+ writeFileSync(TOKEN_FILE, JSON.stringify(stored, null, 2), { mode: 0o600 });
287
+ }
288
+ /**
289
+ * Load stored tokens from disk
290
+ */
291
+ loadStoredTokens() {
292
+ if (!existsSync(TOKEN_FILE)) {
293
+ return null;
294
+ }
295
+ try {
296
+ const content = readFileSync(TOKEN_FILE, 'utf-8');
297
+ if (!content.trim())
298
+ return null;
299
+ const stored = JSON.parse(content);
300
+ // Verify config matches
301
+ if (stored.domain !== this.config.domain || stored.clientId !== this.config.clientId) {
302
+ return null;
303
+ }
304
+ return stored;
305
+ }
306
+ catch {
307
+ return null;
308
+ }
309
+ }
310
+ }
311
+ //# sourceMappingURL=pkce-auth.js.map