retestkit 1.14.0 → 1.15.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.
Files changed (45) hide show
  1. package/README.md +110 -0
  2. package/dist/auth/browser.d.ts +14 -0
  3. package/dist/auth/browser.d.ts.map +1 -0
  4. package/dist/auth/browser.js +67 -0
  5. package/dist/auth/browser.js.map +1 -0
  6. package/dist/auth/device-flow.d.ts +80 -0
  7. package/dist/auth/device-flow.d.ts.map +1 -0
  8. package/dist/auth/device-flow.js +243 -0
  9. package/dist/auth/device-flow.js.map +1 -0
  10. package/dist/auth/ensure-auth.d.ts +74 -0
  11. package/dist/auth/ensure-auth.d.ts.map +1 -0
  12. package/dist/auth/ensure-auth.js +118 -0
  13. package/dist/auth/ensure-auth.js.map +1 -0
  14. package/dist/auth/github-stub.d.ts +10 -4
  15. package/dist/auth/github-stub.d.ts.map +1 -1
  16. package/dist/auth/github-stub.js +10 -4
  17. package/dist/auth/github-stub.js.map +1 -1
  18. package/dist/auth/index.d.ts +19 -0
  19. package/dist/auth/index.d.ts.map +1 -0
  20. package/dist/auth/index.js +19 -0
  21. package/dist/auth/index.js.map +1 -0
  22. package/dist/auth/token-manager.d.ts +71 -0
  23. package/dist/auth/token-manager.d.ts.map +1 -0
  24. package/dist/auth/token-manager.js +201 -0
  25. package/dist/auth/token-manager.js.map +1 -0
  26. package/dist/auth/token-storage.d.ts +80 -0
  27. package/dist/auth/token-storage.d.ts.map +1 -0
  28. package/dist/auth/token-storage.js +244 -0
  29. package/dist/auth/token-storage.js.map +1 -0
  30. package/dist/auth/types.d.ts +189 -0
  31. package/dist/auth/types.d.ts.map +1 -0
  32. package/dist/auth/types.js +61 -0
  33. package/dist/auth/types.js.map +1 -0
  34. package/dist/schemas/file-config.d.ts +10 -0
  35. package/dist/schemas/file-config.d.ts.map +1 -1
  36. package/dist/schemas/file-config.js +23 -0
  37. package/dist/schemas/file-config.js.map +1 -1
  38. package/dist/server.d.ts.map +1 -1
  39. package/dist/server.js +3 -1
  40. package/dist/server.js.map +1 -1
  41. package/dist/tools/auth.d.ts +21 -0
  42. package/dist/tools/auth.d.ts.map +1 -0
  43. package/dist/tools/auth.js +322 -0
  44. package/dist/tools/auth.js.map +1 -0
  45. package/package.json +2 -1
package/README.md CHANGED
@@ -174,8 +174,118 @@ RetestKit uses `.mcp/retestkit.json` for all configuration. This file is created
174
174
 
175
175
  > **Note:** Environment variables are **not supported** for configuration values. Only `RETESTKIT_CONFIG` is recognized (to specify the config file path for CI/CD pipelines).
176
176
 
177
+ ## Authentication
178
+
179
+ RetestKit supports OAuth 2.0 Device Code Flow authentication for accessing protected features. Authentication is handled via the `auth` MCP tool.
180
+
181
+ ### Quick Start
182
+
183
+ ```
184
+ # Check authentication status
185
+ auth({ action: "status" })
186
+
187
+ # Log in (opens browser for authentication)
188
+ auth({ action: "login" })
189
+
190
+ # Log out
191
+ auth({ action: "logout" })
192
+ ```
193
+
194
+ ### How It Works
195
+
196
+ The device code flow is designed for STDIO-based tools that can't receive HTTP callbacks:
197
+
198
+ 1. Call `auth({ action: "login" })` to start the flow
199
+ 2. A browser window opens (or a URL is displayed) for authentication
200
+ 3. Complete login in your browser
201
+ 4. The tool polls for completion and stores tokens securely
202
+
203
+ ### Tool Parameters
204
+
205
+ | Parameter | Type | Default | Description |
206
+ |-----------|------|---------|-------------|
207
+ | `action` | string | `"login"` | Action: `login`, `status`, `logout`, or `continue` |
208
+ | `wait` | boolean | `true` | Wait for login completion (login/continue only) |
209
+ | `waitSeconds` | number | `25` | Max seconds to wait before returning pending status |
210
+
211
+ ### Actions
212
+
213
+ - **`login`** - Start device code flow. If already logged in, returns current status.
214
+ - **`status`** - Check current authentication status without starting a new flow.
215
+ - **`logout`** - Clear stored tokens and end the session.
216
+ - **`continue`** - Resume polling for a pending login (useful for short MCP timeouts).
217
+
218
+ ### Example Responses
219
+
220
+ **Pending login:**
221
+ ```json
222
+ {
223
+ "status": "pending",
224
+ "message": "Open the URL below to complete login...",
225
+ "deviceFlow": {
226
+ "verificationUri": "https://auth.retestkit.dev/device",
227
+ "verificationUriComplete": "https://auth.retestkit.dev/device?code=ABCD-1234",
228
+ "userCode": "ABCD-1234",
229
+ "expiresAt": "2025-01-15T10:15:00Z"
230
+ }
231
+ }
232
+ ```
233
+
234
+ **Logged in:**
235
+ ```json
236
+ {
237
+ "status": "logged_in",
238
+ "message": "Logged in as user@example.com",
239
+ "user": {
240
+ "email": "user@example.com",
241
+ "name": "User Name"
242
+ }
243
+ }
244
+ ```
245
+
246
+ ### Token Storage
247
+
248
+ Tokens are stored securely using:
249
+ 1. **OS Keychain** (preferred) - macOS Keychain, Windows Credential Manager, Linux libsecret
250
+ 2. **Encrypted file** (fallback) - `~/.config/retestkit/tokens.enc` with 0600 permissions
251
+
252
+ ### Auth Configuration
253
+
254
+ Optional auth configuration in `.mcp/retestkit.json`:
255
+
256
+ ```json
257
+ {
258
+ "auth": {
259
+ "authentikBaseUrl": "https://auth.retestkit.dev",
260
+ "clientId": "retestkit-mcp",
261
+ "scopes": "openid profile email offline_access"
262
+ }
263
+ }
264
+ ```
265
+
266
+ > **Note:** Default values work out of the box. Only customize if using a different authentication provider.
267
+
177
268
  ## Available Tools
178
269
 
270
+ ### `auth`
271
+
272
+ Manage authentication for protected RetestKit features.
273
+
274
+ **Input:**
275
+ ```json
276
+ {
277
+ "action": "login",
278
+ "wait": true,
279
+ "waitSeconds": 25
280
+ }
281
+ ```
282
+
283
+ **Actions:** `login` (default), `status`, `logout`, `continue`
284
+
285
+ **Output:** Status, user info (when logged in), device flow info (when pending)
286
+
287
+ See [Authentication](#authentication) section for details.
288
+
179
289
  ### `retest_init`
180
290
 
181
291
  Initialize a new web testing analysis workspace.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Best-effort browser opening utility.
3
+ *
4
+ * Attempts to open a URL in the user's default browser.
5
+ * Fails gracefully if no browser is available.
6
+ */
7
+ /**
8
+ * Attempt to open a URL in the user's default browser.
9
+ *
10
+ * @param url URL to open
11
+ * @returns true if browser was opened, false otherwise
12
+ */
13
+ export declare function openBrowser(url: string): Promise<boolean>;
14
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/auth/browser.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkBH;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA8B/D"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Best-effort browser opening utility.
3
+ *
4
+ * Attempts to open a URL in the user's default browser.
5
+ * Fails gracefully if no browser is available.
6
+ */
7
+ import { exec } from "node:child_process";
8
+ import { promisify } from "node:util";
9
+ const execAsync = promisify(exec);
10
+ /**
11
+ * Platform-specific browser open commands.
12
+ */
13
+ const OPEN_COMMANDS = {
14
+ darwin: ["open"],
15
+ win32: ["cmd", "/c", "start", '""'],
16
+ linux: ["xdg-open"],
17
+ freebsd: ["xdg-open"],
18
+ openbsd: ["xdg-open"],
19
+ };
20
+ /**
21
+ * Attempt to open a URL in the user's default browser.
22
+ *
23
+ * @param url URL to open
24
+ * @returns true if browser was opened, false otherwise
25
+ */
26
+ export async function openBrowser(url) {
27
+ const platform = process.platform;
28
+ const cmdParts = OPEN_COMMANDS[platform];
29
+ if (!cmdParts) {
30
+ // Unsupported platform
31
+ return false;
32
+ }
33
+ try {
34
+ // Validate URL to prevent command injection
35
+ const parsedUrl = new URL(url);
36
+ if (!["http:", "https:"].includes(parsedUrl.protocol)) {
37
+ return false;
38
+ }
39
+ // Escape URL for shell (different escaping per platform)
40
+ const escapedUrl = escapeUrlForPlatform(url, platform);
41
+ const command = [...cmdParts, escapedUrl].join(" ");
42
+ await execAsync(command, {
43
+ timeout: 5000, // 5 second timeout
44
+ windowsHide: true, // Don't show command window on Windows
45
+ });
46
+ return true;
47
+ }
48
+ catch {
49
+ // Browser open failed - this is non-fatal
50
+ return false;
51
+ }
52
+ }
53
+ /**
54
+ * Escape URL for shell command based on platform.
55
+ */
56
+ function escapeUrlForPlatform(url, platform) {
57
+ if (platform === "win32") {
58
+ // Windows: wrap in quotes, escape special characters
59
+ // The URL is already reasonably safe since we validated it
60
+ return `"${url.replace(/"/g, "")}"`;
61
+ }
62
+ else {
63
+ // Unix-like: wrap in single quotes, escape single quotes
64
+ return `'${url.replace(/'/g, "'\\''")}'`;
65
+ }
66
+ }
67
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/auth/browser.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;AAElC;;GAEG;AACH,MAAM,aAAa,GAA6B;IAC9C,MAAM,EAAE,CAAC,MAAM,CAAC;IAChB,KAAK,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC;IACnC,KAAK,EAAE,CAAC,UAAU,CAAC;IACnB,OAAO,EAAE,CAAC,UAAU,CAAC;IACrB,OAAO,EAAE,CAAC,UAAU,CAAC;CACtB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAW;IAC3C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAEzC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,uBAAuB;QACvB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,CAAC;QACH,4CAA4C;QAC5C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,yDAAyD;QACzD,MAAM,UAAU,GAAG,oBAAoB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACvD,MAAM,OAAO,GAAG,CAAC,GAAG,QAAQ,EAAE,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEpD,MAAM,SAAS,CAAC,OAAO,EAAE;YACvB,OAAO,EAAE,IAAI,EAAE,mBAAmB;YAClC,WAAW,EAAE,IAAI,EAAE,uCAAuC;SAC3D,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,0CAA0C;QAC1C,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,oBAAoB,CAAC,GAAW,EAAE,QAAgB;IACzD,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,qDAAqD;QACrD,2DAA2D;QAC3D,OAAO,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC;IACtC,CAAC;SAAM,CAAC;QACN,yDAAyD;QACzD,OAAO,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;IAC3C,CAAC;AACH,CAAC"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * OAuth 2.0 Device Authorization Grant flow manager.
3
+ *
4
+ * Implements RFC 8628 for STDIO-compatible authentication.
5
+ */
6
+ import type { AuthConfig, PendingDeviceFlowSession } from "./types.js";
7
+ import { TokenManager } from "./token-manager.js";
8
+ /**
9
+ * Result of a device flow start operation.
10
+ */
11
+ export interface DeviceFlowStartResult {
12
+ /** User code for display */
13
+ userCode: string;
14
+ /** Verification URL for manual entry */
15
+ verificationUri: string;
16
+ /** Complete verification URL (with embedded code) */
17
+ verificationUriComplete?: string;
18
+ /** When the code expires */
19
+ expiresAt: string;
20
+ /** Whether browser was opened */
21
+ browserOpened: boolean;
22
+ }
23
+ /**
24
+ * Result of polling for token completion.
25
+ */
26
+ export interface DeviceFlowPollResult {
27
+ /** Whether polling completed successfully */
28
+ completed: boolean;
29
+ /** Whether flow is still pending */
30
+ pending: boolean;
31
+ /** Error if flow failed */
32
+ error?: {
33
+ code: string;
34
+ message: string;
35
+ };
36
+ }
37
+ /**
38
+ * Device Flow Manager for handling OAuth device code flow.
39
+ */
40
+ export declare class DeviceFlowManager {
41
+ private config;
42
+ private tokenManager;
43
+ private pendingSession;
44
+ constructor(tokenManager: TokenManager, config?: Partial<AuthConfig>);
45
+ /**
46
+ * Start a new device authorization flow.
47
+ *
48
+ * @param openBrowserWindow Whether to attempt opening the browser
49
+ * @returns Start result with verification URLs and codes
50
+ */
51
+ startFlow(openBrowserWindow?: boolean): Promise<DeviceFlowStartResult>;
52
+ /**
53
+ * Poll for token completion.
54
+ *
55
+ * @param maxWaitSeconds Maximum time to wait
56
+ * @returns Poll result
57
+ */
58
+ pollToken(maxWaitSeconds?: number): Promise<DeviceFlowPollResult>;
59
+ /**
60
+ * Get the current pending session state.
61
+ */
62
+ getPending(): PendingDeviceFlowSession | null;
63
+ /**
64
+ * Clear the pending session.
65
+ */
66
+ clearPending(): void;
67
+ /**
68
+ * Check if there's an active pending session.
69
+ */
70
+ hasPending(): boolean;
71
+ /**
72
+ * Sleep helper for polling.
73
+ */
74
+ private sleep;
75
+ }
76
+ /**
77
+ * Create a device flow manager instance.
78
+ */
79
+ export declare function createDeviceFlowManager(tokenManager: TokenManager, config?: Partial<AuthConfig>): DeviceFlowManager;
80
+ //# sourceMappingURL=device-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,UAAU,EAGV,wBAAwB,EAGzB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AASlD;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IAEjB,wCAAwC;IACxC,eAAe,EAAE,MAAM,CAAC;IAExB,qDAAqD;IACrD,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAEjC,4BAA4B;IAC5B,SAAS,EAAE,MAAM,CAAC;IAElB,iCAAiC;IACjC,aAAa,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,6CAA6C;IAC7C,SAAS,EAAE,OAAO,CAAC;IAEnB,oCAAoC;IACpC,OAAO,EAAE,OAAO,CAAC;IAEjB,2BAA2B;IAC3B,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,cAAc,CAAyC;gBAEnD,YAAY,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC;IAKpE;;;;;OAKG;IACG,SAAS,CAAC,iBAAiB,UAAO,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAuEzE;;;;;OAKG;IACG,SAAS,CAAC,cAAc,SAAK,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAyInE;;OAEG;IACH,UAAU,IAAI,wBAAwB,GAAG,IAAI;IAc7C;;OAEG;IACH,YAAY,IAAI,IAAI;IAIpB;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;OAEG;IACH,OAAO,CAAC,KAAK;CAGd;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,YAAY,EAC1B,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAC3B,iBAAiB,CAEnB"}
@@ -0,0 +1,243 @@
1
+ /**
2
+ * OAuth 2.0 Device Authorization Grant flow manager.
3
+ *
4
+ * Implements RFC 8628 for STDIO-compatible authentication.
5
+ */
6
+ import { AuthError, DEFAULT_AUTH_CONFIG } from "./types.js";
7
+ import { openBrowser } from "./browser.js";
8
+ /** Default poll interval in seconds */
9
+ const DEFAULT_POLL_INTERVAL = 5;
10
+ /** Slow down increment when server requests it */
11
+ const SLOW_DOWN_INCREMENT = 5;
12
+ /**
13
+ * Device Flow Manager for handling OAuth device code flow.
14
+ */
15
+ export class DeviceFlowManager {
16
+ config;
17
+ tokenManager;
18
+ pendingSession = null;
19
+ constructor(tokenManager, config) {
20
+ this.config = { ...DEFAULT_AUTH_CONFIG, ...config };
21
+ this.tokenManager = tokenManager;
22
+ }
23
+ /**
24
+ * Start a new device authorization flow.
25
+ *
26
+ * @param openBrowserWindow Whether to attempt opening the browser
27
+ * @returns Start result with verification URLs and codes
28
+ */
29
+ async startFlow(openBrowserWindow = true) {
30
+ const deviceUrl = `${this.config.authentikBaseUrl}/application/o/device/`;
31
+ const params = new URLSearchParams({
32
+ client_id: this.config.clientId,
33
+ scope: this.config.scopes,
34
+ });
35
+ try {
36
+ const response = await fetch(deviceUrl, {
37
+ method: "POST",
38
+ headers: {
39
+ "Content-Type": "application/x-www-form-urlencoded",
40
+ },
41
+ body: params.toString(),
42
+ });
43
+ if (!response.ok) {
44
+ const errorData = (await response.json().catch(() => ({})));
45
+ throw new AuthError(errorData.error || "device_auth_failed", errorData.error_description || "Failed to start device authorization", true);
46
+ }
47
+ const data = (await response.json());
48
+ // Calculate expiry
49
+ const expiresAt = new Date(Date.now() + data.expires_in * 1000).toISOString();
50
+ // Store pending session
51
+ this.pendingSession = {
52
+ deviceCode: data.device_code,
53
+ userCode: data.user_code,
54
+ verificationUri: data.verification_uri,
55
+ verificationUriComplete: data.verification_uri_complete,
56
+ expiresAt,
57
+ pollInterval: data.interval || DEFAULT_POLL_INTERVAL,
58
+ };
59
+ // Attempt to open browser
60
+ let browserOpened = false;
61
+ if (openBrowserWindow) {
62
+ const urlToOpen = data.verification_uri_complete || data.verification_uri;
63
+ browserOpened = await openBrowser(urlToOpen);
64
+ }
65
+ return {
66
+ userCode: data.user_code,
67
+ verificationUri: data.verification_uri,
68
+ verificationUriComplete: data.verification_uri_complete,
69
+ expiresAt,
70
+ browserOpened,
71
+ };
72
+ }
73
+ catch (error) {
74
+ if (error instanceof AuthError) {
75
+ throw error;
76
+ }
77
+ throw new AuthError("network_error", `Unable to reach auth server: ${error instanceof Error ? error.message : "Unknown error"}`, true);
78
+ }
79
+ }
80
+ /**
81
+ * Poll for token completion.
82
+ *
83
+ * @param maxWaitSeconds Maximum time to wait
84
+ * @returns Poll result
85
+ */
86
+ async pollToken(maxWaitSeconds = 25) {
87
+ if (!this.pendingSession) {
88
+ throw new AuthError("no_pending_session", "No pending device flow session. Start a new login flow.", false);
89
+ }
90
+ // Check if session expired
91
+ if (new Date(this.pendingSession.expiresAt) < new Date()) {
92
+ this.pendingSession = null;
93
+ return {
94
+ completed: false,
95
+ pending: false,
96
+ error: {
97
+ code: "expired_token",
98
+ message: "Login session expired. Please try again.",
99
+ },
100
+ };
101
+ }
102
+ const tokenUrl = `${this.config.authentikBaseUrl}/application/o/token/`;
103
+ const startTime = Date.now();
104
+ const maxWaitMs = maxWaitSeconds * 1000;
105
+ while (Date.now() - startTime < maxWaitMs) {
106
+ // Respect poll interval
107
+ const now = Date.now();
108
+ if (this.pendingSession.lastPollAt) {
109
+ const lastPoll = new Date(this.pendingSession.lastPollAt).getTime();
110
+ const timeSinceLastPoll = now - lastPoll;
111
+ const waitTime = this.pendingSession.pollInterval * 1000 - timeSinceLastPoll;
112
+ if (waitTime > 0) {
113
+ await this.sleep(Math.min(waitTime, maxWaitMs - (now - startTime)));
114
+ if (Date.now() - startTime >= maxWaitMs) {
115
+ break;
116
+ }
117
+ }
118
+ }
119
+ this.pendingSession.lastPollAt = new Date().toISOString();
120
+ const params = new URLSearchParams({
121
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
122
+ client_id: this.config.clientId,
123
+ device_code: this.pendingSession.deviceCode,
124
+ });
125
+ try {
126
+ const response = await fetch(tokenUrl, {
127
+ method: "POST",
128
+ headers: {
129
+ "Content-Type": "application/x-www-form-urlencoded",
130
+ },
131
+ body: params.toString(),
132
+ });
133
+ if (response.ok) {
134
+ // Success! Store tokens and clear pending session
135
+ const tokenResponse = (await response.json());
136
+ await this.tokenManager.storeFromResponse(tokenResponse);
137
+ this.pendingSession = null;
138
+ return {
139
+ completed: true,
140
+ pending: false,
141
+ };
142
+ }
143
+ const errorData = (await response.json());
144
+ const errorCode = errorData.error;
145
+ switch (errorCode) {
146
+ case "authorization_pending":
147
+ // Still waiting - continue polling
148
+ break;
149
+ case "slow_down":
150
+ // Increase poll interval
151
+ this.pendingSession.pollInterval += SLOW_DOWN_INCREMENT;
152
+ break;
153
+ case "expired_token":
154
+ this.pendingSession = null;
155
+ return {
156
+ completed: false,
157
+ pending: false,
158
+ error: {
159
+ code: "expired_token",
160
+ message: "Login session expired. Please try again.",
161
+ },
162
+ };
163
+ case "access_denied":
164
+ this.pendingSession = null;
165
+ return {
166
+ completed: false,
167
+ pending: false,
168
+ error: {
169
+ code: "access_denied",
170
+ message: "Login was denied. Please try again.",
171
+ },
172
+ };
173
+ default:
174
+ // Unknown error - clear session and report
175
+ this.pendingSession = null;
176
+ return {
177
+ completed: false,
178
+ pending: false,
179
+ error: {
180
+ code: errorCode || "unknown",
181
+ message: errorData.error_description || "Login failed.",
182
+ },
183
+ };
184
+ }
185
+ }
186
+ catch (error) {
187
+ // Network error during poll - don't clear session, allow retry
188
+ return {
189
+ completed: false,
190
+ pending: true,
191
+ error: {
192
+ code: "network_error",
193
+ message: `Unable to reach auth server: ${error instanceof Error ? error.message : "Unknown error"}`,
194
+ },
195
+ };
196
+ }
197
+ }
198
+ // Timeout - session still pending
199
+ return {
200
+ completed: false,
201
+ pending: true,
202
+ };
203
+ }
204
+ /**
205
+ * Get the current pending session state.
206
+ */
207
+ getPending() {
208
+ if (!this.pendingSession) {
209
+ return null;
210
+ }
211
+ // Check if expired
212
+ if (new Date(this.pendingSession.expiresAt) < new Date()) {
213
+ this.pendingSession = null;
214
+ return null;
215
+ }
216
+ return { ...this.pendingSession };
217
+ }
218
+ /**
219
+ * Clear the pending session.
220
+ */
221
+ clearPending() {
222
+ this.pendingSession = null;
223
+ }
224
+ /**
225
+ * Check if there's an active pending session.
226
+ */
227
+ hasPending() {
228
+ return this.getPending() !== null;
229
+ }
230
+ /**
231
+ * Sleep helper for polling.
232
+ */
233
+ sleep(ms) {
234
+ return new Promise((resolve) => setTimeout(resolve, ms));
235
+ }
236
+ }
237
+ /**
238
+ * Create a device flow manager instance.
239
+ */
240
+ export function createDeviceFlowManager(tokenManager, config) {
241
+ return new DeviceFlowManager(tokenManager, config);
242
+ }
243
+ //# sourceMappingURL=device-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-flow.js","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Authentication guard for protected tools.
3
+ *
4
+ * Provides a wrapper that checks authentication status before
5
+ * allowing tool execution.
6
+ */
7
+ import type { ToolResult } from "../types/tool.js";
8
+ import { type AuthConfig } from "./types.js";
9
+ /**
10
+ * Result of auth check.
11
+ */
12
+ export interface AuthCheckResult {
13
+ /** Whether the user is authenticated */
14
+ authenticated: boolean;
15
+ /** Access token if authenticated */
16
+ accessToken?: string;
17
+ /** Error message if not authenticated */
18
+ error?: string;
19
+ }
20
+ /**
21
+ * Response to return when auth is required.
22
+ */
23
+ export interface AuthRequiredResponse {
24
+ authRequired: true;
25
+ message: string;
26
+ action: string;
27
+ toolName: string;
28
+ }
29
+ /**
30
+ * Check if the user is authenticated.
31
+ *
32
+ * @param config Optional auth configuration
33
+ * @returns Auth check result
34
+ */
35
+ export declare function checkAuth(config?: Partial<AuthConfig>): Promise<AuthCheckResult>;
36
+ /**
37
+ * Create an "auth required" response for a protected tool.
38
+ *
39
+ * @param toolName Name of the tool that requires auth
40
+ * @returns Tool result indicating auth is required
41
+ */
42
+ export declare function createAuthRequiredResponse(toolName: string): ToolResult;
43
+ /**
44
+ * Wrapper that ensures authentication before executing a tool handler.
45
+ *
46
+ * Usage:
47
+ * ```typescript
48
+ * const protectedHandler = ensureAuth(
49
+ * "protected_tool",
50
+ * async (input, accessToken) => {
51
+ * // Tool implementation with access to token
52
+ * return { content: [...] };
53
+ * }
54
+ * );
55
+ * ```
56
+ *
57
+ * @param toolName Name of the tool for error messages
58
+ * @param handler Tool handler to wrap
59
+ * @param config Optional auth configuration
60
+ * @returns Wrapped handler that checks auth first
61
+ */
62
+ export declare function ensureAuth<TInput>(toolName: string, handler: (input: TInput, accessToken: string) => Promise<ToolResult>, config?: Partial<AuthConfig>): (input: TInput) => Promise<ToolResult>;
63
+ /**
64
+ * Get the current access token if authenticated.
65
+ *
66
+ * @param config Optional auth configuration
67
+ * @returns Access token or null if not authenticated
68
+ */
69
+ export declare function getAccessToken(config?: Partial<AuthConfig>): Promise<string | null>;
70
+ /**
71
+ * Reset token manager instance (for testing).
72
+ */
73
+ export declare function resetTokenManager(): void;
74
+ //# sourceMappingURL=ensure-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ensure-auth.d.ts","sourceRoot":"","sources":["../../src/auth/ensure-auth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAElE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,wCAAwC;IACxC,aAAa,EAAE,OAAO,CAAC;IAEvB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAkBD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAwBtF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAgBvE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,EACpE,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAC3B,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAUxC;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGzF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}