@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.
- package/README.md +303 -0
- package/dist/api/complai-client.d.ts +46 -0
- package/dist/api/complai-client.d.ts.map +1 -0
- package/dist/api/complai-client.js +113 -0
- package/dist/api/complai-client.js.map +1 -0
- package/dist/auth/device-auth.d.ts +98 -0
- package/dist/auth/device-auth.d.ts.map +1 -0
- package/dist/auth/device-auth.js +274 -0
- package/dist/auth/device-auth.js.map +1 -0
- package/dist/auth/pkce-auth.d.ts +67 -0
- package/dist/auth/pkce-auth.d.ts.map +1 -0
- package/dist/auth/pkce-auth.js +311 -0
- package/dist/auth/pkce-auth.js.map +1 -0
- package/dist/auth/token-manager.d.ts +58 -0
- package/dist/auth/token-manager.d.ts.map +1 -0
- package/dist/auth/token-manager.js +131 -0
- package/dist/auth/token-manager.js.map +1 -0
- package/dist/config/config-loader.d.ts +21 -0
- package/dist/config/config-loader.d.ts.map +1 -0
- package/dist/config/config-loader.js +152 -0
- package/dist/config/config-loader.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +483 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/compliance-brief.d.ts +8 -0
- package/dist/tools/compliance-brief.d.ts.map +1 -0
- package/dist/tools/compliance-brief.js +122 -0
- package/dist/tools/compliance-brief.js.map +1 -0
- package/dist/types.d.ts +169 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -0
|
@@ -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
|