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.
- package/README.md +206 -16
- package/dist/auth/browser.d.ts +14 -0
- package/dist/auth/browser.d.ts.map +1 -0
- package/dist/auth/browser.js +67 -0
- package/dist/auth/browser.js.map +1 -0
- package/dist/auth/device-flow.d.ts +80 -0
- package/dist/auth/device-flow.d.ts.map +1 -0
- package/dist/auth/device-flow.js +243 -0
- package/dist/auth/device-flow.js.map +1 -0
- package/dist/auth/ensure-auth.d.ts +74 -0
- package/dist/auth/ensure-auth.d.ts.map +1 -0
- package/dist/auth/ensure-auth.js +118 -0
- package/dist/auth/ensure-auth.js.map +1 -0
- package/dist/auth/github-stub.d.ts +10 -4
- package/dist/auth/github-stub.d.ts.map +1 -1
- package/dist/auth/github-stub.js +10 -4
- package/dist/auth/github-stub.js.map +1 -1
- package/dist/auth/index.d.ts +19 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +19 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/token-manager.d.ts +71 -0
- package/dist/auth/token-manager.d.ts.map +1 -0
- package/dist/auth/token-manager.js +201 -0
- package/dist/auth/token-manager.js.map +1 -0
- package/dist/auth/token-storage.d.ts +80 -0
- package/dist/auth/token-storage.d.ts.map +1 -0
- package/dist/auth/token-storage.js +244 -0
- package/dist/auth/token-storage.js.map +1 -0
- package/dist/auth/types.d.ts +189 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +61 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +47 -51
- package/dist/config.js.map +1 -1
- package/dist/schemas/config.d.ts +1 -1
- package/dist/schemas/config.d.ts.map +1 -1
- package/dist/schemas/config.js +8 -11
- package/dist/schemas/config.js.map +1 -1
- package/dist/schemas/file-config.d.ts +37 -2
- package/dist/schemas/file-config.d.ts.map +1 -1
- package/dist/schemas/file-config.js +57 -3
- package/dist/schemas/file-config.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/auth.d.ts +21 -0
- package/dist/tools/auth.d.ts.map +1 -0
- package/dist/tools/auth.js +322 -0
- package/dist/tools/auth.js.map +1 -0
- 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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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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"}
|