retestkit 1.13.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 (53) hide show
  1. package/README.md +206 -16
  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/config.d.ts +2 -1
  35. package/dist/config.d.ts.map +1 -1
  36. package/dist/config.js +47 -51
  37. package/dist/config.js.map +1 -1
  38. package/dist/schemas/config.d.ts +1 -1
  39. package/dist/schemas/config.d.ts.map +1 -1
  40. package/dist/schemas/config.js +8 -11
  41. package/dist/schemas/config.js.map +1 -1
  42. package/dist/schemas/file-config.d.ts +37 -2
  43. package/dist/schemas/file-config.d.ts.map +1 -1
  44. package/dist/schemas/file-config.js +57 -3
  45. package/dist/schemas/file-config.js.map +1 -1
  46. package/dist/server.d.ts.map +1 -1
  47. package/dist/server.js +3 -1
  48. package/dist/server.js.map +1 -1
  49. package/dist/tools/auth.d.ts +21 -0
  50. package/dist/tools/auth.d.ts.map +1 -0
  51. package/dist/tools/auth.js +322 -0
  52. package/dist/tools/auth.js.map +1 -0
  53. package/package.json +2 -1
package/README.md CHANGED
@@ -75,27 +75,217 @@ npm run test:e2e
75
75
 
76
76
  E2E tests have extended timeouts (5 minutes per test) and run serially to avoid resource contention. They create isolated temporary workspaces that are cleaned up after each test.
77
77
 
78
+ ## Quick Start
79
+
80
+ RetestKit is designed for **zero-configuration installation**. Just add the server to your MCP client and run `/init` to configure.
81
+
82
+ ### Adding to MCP Clients
83
+
84
+ **VS Code / GitHub Copilot** (`.vscode/mcp.json`):
85
+ ```json
86
+ {
87
+ "servers": {
88
+ "retestkit": {
89
+ "type": "stdio",
90
+ "command": "npx",
91
+ "args": ["retestkit"]
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ **Claude Desktop** (`claude_desktop_config.json`):
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "retestkit": {
102
+ "command": "npx",
103
+ "args": ["retestkit"]
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ **Cursor / Cline** (settings):
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "retestkit": {
114
+ "command": "npx",
115
+ "args": ["retestkit"]
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ > **Note:** No `env` or `inputs` sections needed. All configuration happens via the `/init` prompt after the server starts.
122
+
123
+ ### First-Time Setup
124
+
125
+ After adding the server to your MCP client:
126
+
127
+ 1. **Run `/init`** - The init prompt guides you through configuration
128
+ 2. **Provide target URL** - The URL of the web application to test
129
+ 3. **Configuration saved** - Settings are stored in `.mcp/retestkit.json`
130
+
131
+ The `/init` prompt handles:
132
+ - Creating the config file (`.mcp/retestkit.json`)
133
+ - Setting up shortcuts (e.g., `/retest` command)
134
+ - Guiding you to start testing with `/cover`
135
+
78
136
  ## Configuration
79
137
 
80
- Configuration is done via environment variables:
81
-
82
- | Variable | Default | Description |
83
- |----------|---------|-------------|
84
- | `TRANSPORT` | `stdio` | Transport type: `stdio` or `http` |
85
- | `PORT` | `3000` | HTTP port (when `TRANSPORT=http`) |
86
- | `LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
87
- | `RETEST_WORKSPACE_DIR` | `./retest` | Directory for analysis workspaces |
88
- | `PLAYWRIGHT_MCP_COMMAND` | `npx` | Command to spawn Playwright MCP |
89
- | `PLAYWRIGHT_MCP_ARGS` | `@playwright/mcp@latest` | Comma-separated args for Playwright MCP |
90
- | `CHECKPOINT_INTERVAL` | `5` | Steps between crawl checkpoints |
91
- | `SCREENSHOT_FORMAT` | `png` | Screenshot format: `png` or `jpeg` |
92
- | `SCREENSHOT_QUALITY` | `80` | JPEG quality (1-100) |
93
- | `DEFAULT_MAX_STEPS` | `50` | Default max crawl steps |
94
- | `DEFAULT_MAX_MINUTES` | `30` | Default max crawl duration |
95
- | `DEFAULT_MAX_PAGES` | `20` | Default max pages to crawl |
138
+ RetestKit uses `.mcp/retestkit.json` for all configuration. This file is created automatically when you run `/init`.
139
+
140
+ **Example `.mcp/retestkit.json`:**
141
+ ```json
142
+ {
143
+ "version": 1,
144
+ "targetUrl": "https://myapp.com",
145
+ "specsPath": "./specs",
146
+ "allowedDomains": ["myapp.com", "*.myapp.com"],
147
+ "transport": "stdio",
148
+ "port": 3000,
149
+ "workspaceDir": "./retest",
150
+ "limits": {
151
+ "maxSteps": 50,
152
+ "maxMinutes": 30,
153
+ "maxPages": 20
154
+ },
155
+ "logging": {
156
+ "level": "info"
157
+ },
158
+ "playwright": {
159
+ "command": "npx",
160
+ "args": ["@playwright/mcp@latest"]
161
+ },
162
+ "checkpointInterval": 5,
163
+ "screenshotFormat": "png",
164
+ "screenshotQuality": 80
165
+ }
166
+ ```
167
+
168
+ **Config file discovery order:**
169
+ 1. CLI flag: `--config /path/to/config.json`
170
+ 2. Environment: `RETESTKIT_CONFIG=/path/to/config.json`
171
+ 3. Workspace: `./.mcp/retestkit.json`
172
+ 4. User-level: `~/.config/retestkit/config.json`
173
+ 5. Built-in defaults
174
+
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
+
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.
96
267
 
97
268
  ## Available Tools
98
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
+
99
289
  ### `retest_init`
100
290
 
101
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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"}