@provartesting/provardx-cli 1.5.0-dev.1 → 1.5.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 +163 -13
- package/bin/mcp-start.js +74 -0
- package/lib/commands/provar/auth/clear.d.ts +7 -0
- package/lib/commands/provar/auth/clear.js +36 -0
- package/lib/commands/provar/auth/clear.js.map +1 -0
- package/lib/commands/provar/auth/login.d.ts +10 -0
- package/lib/commands/provar/auth/login.js +90 -0
- package/lib/commands/provar/auth/login.js.map +1 -0
- package/lib/commands/provar/auth/rotate.d.ts +7 -0
- package/lib/commands/provar/auth/rotate.js +42 -0
- package/lib/commands/provar/auth/rotate.js.map +1 -0
- package/lib/commands/provar/auth/status.d.ts +7 -0
- package/lib/commands/provar/auth/status.js +107 -0
- package/lib/commands/provar/auth/status.js.map +1 -0
- package/lib/commands/provar/mcp/start.d.ts +2 -0
- package/lib/commands/provar/mcp/start.js +14 -1
- package/lib/commands/provar/mcp/start.js.map +1 -1
- package/lib/mcp/docs/NITROX_CATALOG_SOURCE.json +6 -0
- package/lib/mcp/docs/NITROX_COMPONENT_CATALOG.md +2001 -0
- package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +1430 -0
- package/lib/mcp/docs/PROVAR_TOOL_GUIDE.md +175 -0
- package/lib/mcp/licensing/algasClient.js +14 -5
- package/lib/mcp/licensing/algasClient.js.map +1 -1
- package/lib/mcp/licensing/ideDetection.d.ts +0 -12
- package/lib/mcp/licensing/ideDetection.js +1 -73
- package/lib/mcp/licensing/ideDetection.js.map +1 -1
- package/lib/mcp/licensing/licenseCache.js +7 -1
- package/lib/mcp/licensing/licenseCache.js.map +1 -1
- package/lib/mcp/licensing/licenseValidator.d.ts +3 -3
- package/lib/mcp/licensing/licenseValidator.js +11 -4
- package/lib/mcp/licensing/licenseValidator.js.map +1 -1
- package/lib/mcp/prompts/guidePrompts.d.ts +4 -0
- package/lib/mcp/prompts/guidePrompts.js +324 -0
- package/lib/mcp/prompts/guidePrompts.js.map +1 -0
- package/lib/mcp/prompts/index.d.ts +2 -0
- package/lib/mcp/prompts/index.js +23 -0
- package/lib/mcp/prompts/index.js.map +1 -0
- package/lib/mcp/prompts/loopPrompts.d.ts +6 -0
- package/lib/mcp/prompts/loopPrompts.js +435 -0
- package/lib/mcp/prompts/loopPrompts.js.map +1 -0
- package/lib/mcp/prompts/migrationPrompts.d.ts +4 -0
- package/lib/mcp/prompts/migrationPrompts.js +207 -0
- package/lib/mcp/prompts/migrationPrompts.js.map +1 -0
- package/lib/mcp/rules/provar_best_practices_rules.json +256 -544
- package/lib/mcp/security/pathPolicy.d.ts +5 -0
- package/lib/mcp/security/pathPolicy.js +58 -3
- package/lib/mcp/security/pathPolicy.js.map +1 -1
- package/lib/mcp/server.d.ts +17 -0
- package/lib/mcp/server.js +151 -6
- package/lib/mcp/server.js.map +1 -1
- package/lib/mcp/tools/antTools.d.ts +15 -0
- package/lib/mcp/tools/antTools.js +347 -170
- package/lib/mcp/tools/antTools.js.map +1 -1
- package/lib/mcp/tools/automationTools.d.ts +18 -8
- package/lib/mcp/tools/automationTools.js +332 -176
- package/lib/mcp/tools/automationTools.js.map +1 -1
- package/lib/mcp/tools/bestPracticesEngine.js +161 -23
- package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
- package/lib/mcp/tools/connectionTools.d.ts +4 -0
- package/lib/mcp/tools/connectionTools.js +172 -0
- package/lib/mcp/tools/connectionTools.js.map +1 -0
- package/lib/mcp/tools/defectTools.d.ts +1 -1
- package/lib/mcp/tools/defectTools.js +56 -50
- package/lib/mcp/tools/defectTools.js.map +1 -1
- package/lib/mcp/tools/hierarchyValidate.d.ts +1 -1
- package/lib/mcp/tools/hierarchyValidate.js +127 -42
- package/lib/mcp/tools/hierarchyValidate.js.map +1 -1
- package/lib/mcp/tools/nitroXTools.d.ts +23 -0
- package/lib/mcp/tools/nitroXTools.js +823 -0
- package/lib/mcp/tools/nitroXTools.js.map +1 -0
- package/lib/mcp/tools/pageObjectGenerate.js +132 -57
- package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
- package/lib/mcp/tools/pageObjectValidate.js +136 -46
- package/lib/mcp/tools/pageObjectValidate.js.map +1 -1
- package/lib/mcp/tools/projectInspect.js +51 -30
- package/lib/mcp/tools/projectInspect.js.map +1 -1
- package/lib/mcp/tools/projectValidateFromPath.js +70 -49
- package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
- package/lib/mcp/tools/propertiesTools.d.ts +2 -0
- package/lib/mcp/tools/propertiesTools.js +332 -78
- package/lib/mcp/tools/propertiesTools.js.map +1 -1
- package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
- package/lib/mcp/tools/qualityHubApiTools.js +138 -0
- package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
- package/lib/mcp/tools/qualityHubTools.js +219 -70
- package/lib/mcp/tools/qualityHubTools.js.map +1 -1
- package/lib/mcp/tools/rcaTools.d.ts +3 -2
- package/lib/mcp/tools/rcaTools.js +189 -56
- package/lib/mcp/tools/rcaTools.js.map +1 -1
- package/lib/mcp/tools/sfSpawn.d.ts +25 -3
- package/lib/mcp/tools/sfSpawn.js +154 -6
- package/lib/mcp/tools/sfSpawn.js.map +1 -1
- package/lib/mcp/tools/testCaseGenerate.js +226 -78
- package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
- package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
- package/lib/mcp/tools/testCaseStepTools.js +226 -0
- package/lib/mcp/tools/testCaseStepTools.js.map +1 -0
- package/lib/mcp/tools/testCaseValidate.d.ts +11 -0
- package/lib/mcp/tools/testCaseValidate.js +307 -46
- package/lib/mcp/tools/testCaseValidate.js.map +1 -1
- package/lib/mcp/tools/testPlanTools.d.ts +1 -0
- package/lib/mcp/tools/testPlanTools.js +299 -59
- package/lib/mcp/tools/testPlanTools.js.map +1 -1
- package/lib/mcp/tools/testPlanValidate.js +56 -18
- package/lib/mcp/tools/testPlanValidate.js.map +1 -1
- package/lib/mcp/tools/testSuiteValidate.js +37 -11
- package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
- package/lib/mcp/update/updateChecker.d.ts +14 -0
- package/lib/mcp/update/updateChecker.js +228 -0
- package/lib/mcp/update/updateChecker.js.map +1 -0
- package/lib/services/auth/credentials.d.ts +21 -0
- package/lib/services/auth/credentials.js +75 -0
- package/lib/services/auth/credentials.js.map +1 -0
- package/lib/services/auth/loginFlow.d.ts +68 -0
- package/lib/services/auth/loginFlow.js +216 -0
- package/lib/services/auth/loginFlow.js.map +1 -0
- package/lib/services/projectValidation.d.ts +5 -2
- package/lib/services/projectValidation.js +83 -31
- package/lib/services/projectValidation.js.map +1 -1
- package/lib/services/qualityHub/client.d.ts +161 -0
- package/lib/services/qualityHub/client.js +226 -0
- package/lib/services/qualityHub/client.js.map +1 -0
- package/messages/sf.provar.auth.clear.md +16 -0
- package/messages/sf.provar.auth.login.md +31 -0
- package/messages/sf.provar.auth.rotate.md +23 -0
- package/messages/sf.provar.auth.status.md +16 -0
- package/messages/sf.provar.mcp.start.md +83 -48
- package/oclif.manifest.json +299 -2
- package/package.json +23 -12
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
export declare const CALLBACK_PORTS: number[];
|
|
3
|
+
/**
|
|
4
|
+
* Generate a PKCE code_verifier / code_challenge pair (S256 method, as required by Cognito).
|
|
5
|
+
*/
|
|
6
|
+
export declare function generatePkce(): {
|
|
7
|
+
verifier: string;
|
|
8
|
+
challenge: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Generate a random nonce for OIDC replay-attack prevention.
|
|
12
|
+
* Required by the OpenID Connect spec when requesting an id_token.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateNonce(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Generate a random state value for CSRF protection.
|
|
17
|
+
* Required by Cognito Managed Login even though it is optional per the OAuth 2.0 spec.
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateState(): string;
|
|
20
|
+
/**
|
|
21
|
+
* Try each registered callback port in order; return the first that is free.
|
|
22
|
+
*/
|
|
23
|
+
export declare function findAvailablePort(): Promise<number>;
|
|
24
|
+
/**
|
|
25
|
+
* Return the platform-specific command and argument list for opening a URL
|
|
26
|
+
* in the system browser. The URL is passed as an argument — never interpolated
|
|
27
|
+
* into a shell string — to avoid command injection. Exported so tests can
|
|
28
|
+
* assert the correct command is chosen without actually spawning a process.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getBrowserCommand(url: string, platform?: NodeJS.Platform): {
|
|
31
|
+
cmd: string;
|
|
32
|
+
args: string[];
|
|
33
|
+
};
|
|
34
|
+
export declare function openBrowser(url: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Spin up a temporary localhost HTTP server that accepts exactly one callback
|
|
37
|
+
* from Cognito's Hosted UI, extracts the auth code, and shuts down.
|
|
38
|
+
*/
|
|
39
|
+
export declare function listenForCallback(port: number, expectedState?: string): Promise<string>;
|
|
40
|
+
export interface CognitoTokens {
|
|
41
|
+
access_token: string;
|
|
42
|
+
id_token: string;
|
|
43
|
+
token_type: string;
|
|
44
|
+
expires_in: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Exchange a PKCE auth code for Cognito tokens via the standard token endpoint.
|
|
48
|
+
* Uses the Authorization Code + PKCE grant — no client secret required.
|
|
49
|
+
*/
|
|
50
|
+
export declare function exchangeCodeForTokens(opts: {
|
|
51
|
+
code: string;
|
|
52
|
+
redirectUri: string;
|
|
53
|
+
clientId: string;
|
|
54
|
+
verifier: string;
|
|
55
|
+
tokenEndpoint: string;
|
|
56
|
+
}): Promise<CognitoTokens>;
|
|
57
|
+
/**
|
|
58
|
+
* The login command calls loginFlowClient.X() so tests can replace properties with stubs.
|
|
59
|
+
*/
|
|
60
|
+
export declare const loginFlowClient: {
|
|
61
|
+
generatePkce: typeof generatePkce;
|
|
62
|
+
generateNonce: typeof generateNonce;
|
|
63
|
+
generateState: typeof generateState;
|
|
64
|
+
findAvailablePort: typeof findAvailablePort;
|
|
65
|
+
openBrowser: typeof openBrowser;
|
|
66
|
+
listenForCallback: (port: number, expectedState?: string) => Promise<string>;
|
|
67
|
+
exchangeCodeForTokens: typeof exchangeCodeForTokens;
|
|
68
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024 Provar Limited.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* Licensed under the BSD 3-Clause license.
|
|
5
|
+
* For full license text, see LICENSE.md file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
6
|
+
*/
|
|
7
|
+
/* eslint-disable camelcase */
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import http from 'node:http';
|
|
10
|
+
import https from 'node:https';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { URL } from 'node:url';
|
|
13
|
+
// All three ports must be pre-registered in both the Cognito App Client and the Salesforce External Client Application (SF ECA).
|
|
14
|
+
// Both providers require redirect_uri to exactly match a registered callback URL — no wildcards.
|
|
15
|
+
export const CALLBACK_PORTS = [1717, 7890, 8080];
|
|
16
|
+
// ── PKCE ─────────────────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Generate a PKCE code_verifier / code_challenge pair (S256 method, as required by Cognito).
|
|
19
|
+
*/
|
|
20
|
+
export function generatePkce() {
|
|
21
|
+
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
22
|
+
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
23
|
+
return { verifier, challenge };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generate a random nonce for OIDC replay-attack prevention.
|
|
27
|
+
* Required by the OpenID Connect spec when requesting an id_token.
|
|
28
|
+
*/
|
|
29
|
+
export function generateNonce() {
|
|
30
|
+
return crypto.randomBytes(16).toString('base64url');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Generate a random state value for CSRF protection.
|
|
34
|
+
* Required by Cognito Managed Login even though it is optional per the OAuth 2.0 spec.
|
|
35
|
+
*/
|
|
36
|
+
export function generateState() {
|
|
37
|
+
return crypto.randomBytes(16).toString('base64url');
|
|
38
|
+
}
|
|
39
|
+
// ── Port selection ────────────────────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Try each registered callback port in order; return the first that is free.
|
|
42
|
+
*/
|
|
43
|
+
export async function findAvailablePort() {
|
|
44
|
+
for (const port of CALLBACK_PORTS) {
|
|
45
|
+
// Sequential by design — we need the first free registered port, not all of them.
|
|
46
|
+
// eslint-disable-next-line no-await-in-loop
|
|
47
|
+
if (await isPortFree(port))
|
|
48
|
+
return port;
|
|
49
|
+
}
|
|
50
|
+
throw new Error('Could not bind to any registered callback port (1717, 7890, 8080). ' +
|
|
51
|
+
'Check that no other process is using these ports and try again.');
|
|
52
|
+
}
|
|
53
|
+
function isPortFree(port) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const probe = http.createServer();
|
|
56
|
+
probe.once('error', () => resolve(false));
|
|
57
|
+
probe.listen(port, '127.0.0.1', () => {
|
|
58
|
+
probe.close(() => resolve(true));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// ── Browser open ──────────────────────────────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Return the platform-specific command and argument list for opening a URL
|
|
65
|
+
* in the system browser. The URL is passed as an argument — never interpolated
|
|
66
|
+
* into a shell string — to avoid command injection. Exported so tests can
|
|
67
|
+
* assert the correct command is chosen without actually spawning a process.
|
|
68
|
+
*/
|
|
69
|
+
export function getBrowserCommand(url, platform = process.platform) {
|
|
70
|
+
switch (platform) {
|
|
71
|
+
case 'darwin':
|
|
72
|
+
return { cmd: 'open', args: [url] };
|
|
73
|
+
case 'win32':
|
|
74
|
+
// Pass the URL via $args[0] so it is never interpolated into the -Command
|
|
75
|
+
// string — avoids quote-breaking and injection risk from special characters.
|
|
76
|
+
return { cmd: 'powershell.exe', args: ['-NoProfile', '-Command', 'Start-Process $args[0]', '-args', url] };
|
|
77
|
+
default:
|
|
78
|
+
return { cmd: 'xdg-open', args: [url] };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function openBrowser(url) {
|
|
82
|
+
// detached:true + stdio:'ignore' + unref() is the standard Node.js pattern for
|
|
83
|
+
// fire-and-forget child processes — the event loop will not wait for them to exit.
|
|
84
|
+
const { cmd, args } = getBrowserCommand(url);
|
|
85
|
+
const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
|
|
86
|
+
// Suppress unhandled-error crashes if the browser executable is not found.
|
|
87
|
+
// The login URL is already printed to the terminal so the user can open it manually.
|
|
88
|
+
child.on('error', () => {
|
|
89
|
+
/* intentional no-op */
|
|
90
|
+
});
|
|
91
|
+
child.unref();
|
|
92
|
+
}
|
|
93
|
+
// ── Localhost callback server ─────────────────────────────────────────────────
|
|
94
|
+
/**
|
|
95
|
+
* Spin up a temporary localhost HTTP server that accepts exactly one callback
|
|
96
|
+
* from Cognito's Hosted UI, extracts the auth code, and shuts down.
|
|
97
|
+
*/
|
|
98
|
+
export function listenForCallback(port, expectedState) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
// Track open sockets so we can forcibly destroy them on shutdown.
|
|
101
|
+
// This is a Node 18.0/18.1 fallback for server.closeAllConnections(), which
|
|
102
|
+
// was added in Node 18.2. Without it a browser that ignores Connection:close
|
|
103
|
+
// could keep the event loop alive after server.close() returns.
|
|
104
|
+
const openSockets = new Set();
|
|
105
|
+
const closeServer = (srv) => {
|
|
106
|
+
srv.close();
|
|
107
|
+
if (typeof srv.closeAllConnections === 'function') {
|
|
108
|
+
srv.closeAllConnections();
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
openSockets.forEach((s) => s.destroy());
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const server = http.createServer((req, res) => {
|
|
115
|
+
const parsed = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
116
|
+
const code = parsed.searchParams.get('code');
|
|
117
|
+
const error = parsed.searchParams.get('error');
|
|
118
|
+
const description = parsed.searchParams.get('error_description');
|
|
119
|
+
const callbackState = parsed.searchParams.get('state');
|
|
120
|
+
if (expectedState && callbackState !== expectedState) {
|
|
121
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8', Connection: 'close' });
|
|
122
|
+
res.end('<html><body style="font-family:sans-serif;padding:2rem;max-width:480px">' +
|
|
123
|
+
'<h2 style="color:#c23934">Authentication failed</h2>' +
|
|
124
|
+
'<p>Invalid state parameter — possible CSRF attack. Please try again.</p>' +
|
|
125
|
+
'</body></html>');
|
|
126
|
+
closeServer(server);
|
|
127
|
+
reject(new Error('OAuth callback state mismatch — possible CSRF. Try again.'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// 'Connection: close' tells the browser to close the TCP connection after
|
|
131
|
+
// this response so server.close() has no lingering keep-alive sockets to
|
|
132
|
+
// wait for, allowing the Node.js event loop to exit promptly.
|
|
133
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', Connection: 'close' });
|
|
134
|
+
res.end('<html><body style="font-family:sans-serif;padding:2rem;max-width:480px">' +
|
|
135
|
+
'<h2 style="color:#0070d2">Authentication complete</h2>' +
|
|
136
|
+
'<p>You can close this tab and return to the terminal.</p>' +
|
|
137
|
+
'</body></html>');
|
|
138
|
+
closeServer(server);
|
|
139
|
+
if (code) {
|
|
140
|
+
resolve(code);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
reject(new Error(description ?? error ?? 'No authorisation code received from identity provider'));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
server.on('connection', (socket) => {
|
|
147
|
+
openSockets.add(socket);
|
|
148
|
+
socket.once('close', () => openSockets.delete(socket));
|
|
149
|
+
});
|
|
150
|
+
server.listen(port, '127.0.0.1');
|
|
151
|
+
server.on('error', (err) => reject(err));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Exchange a PKCE auth code for Cognito tokens via the standard token endpoint.
|
|
156
|
+
* Uses the Authorization Code + PKCE grant — no client secret required.
|
|
157
|
+
*/
|
|
158
|
+
export async function exchangeCodeForTokens(opts) {
|
|
159
|
+
const body = new URLSearchParams({
|
|
160
|
+
grant_type: 'authorization_code',
|
|
161
|
+
code: opts.code,
|
|
162
|
+
redirect_uri: opts.redirectUri,
|
|
163
|
+
client_id: opts.clientId,
|
|
164
|
+
code_verifier: opts.verifier,
|
|
165
|
+
}).toString();
|
|
166
|
+
const { status, responseBody } = await httpsPost(opts.tokenEndpoint, body, {
|
|
167
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
168
|
+
});
|
|
169
|
+
if (status !== 200) {
|
|
170
|
+
throw new Error(`Token exchange failed (${status}): ${responseBody}`);
|
|
171
|
+
}
|
|
172
|
+
return JSON.parse(responseBody);
|
|
173
|
+
}
|
|
174
|
+
// ── Internal HTTPS helper ─────────────────────────────────────────────────────
|
|
175
|
+
const REQUEST_TIMEOUT_MS = 30000;
|
|
176
|
+
function httpsPost(url, body, headers) {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const parsed = new URL(url);
|
|
179
|
+
const req = https.request({
|
|
180
|
+
hostname: parsed.hostname,
|
|
181
|
+
port: parsed.port || undefined,
|
|
182
|
+
path: parsed.pathname + parsed.search,
|
|
183
|
+
method: 'POST',
|
|
184
|
+
headers: {
|
|
185
|
+
...headers,
|
|
186
|
+
'Content-Length': Buffer.byteLength(body).toString(),
|
|
187
|
+
},
|
|
188
|
+
}, (res) => {
|
|
189
|
+
let data = '';
|
|
190
|
+
res.on('data', (chunk) => {
|
|
191
|
+
data += chunk.toString('utf-8');
|
|
192
|
+
});
|
|
193
|
+
res.on('end', () => resolve({ status: res.statusCode ?? 0, responseBody: data }));
|
|
194
|
+
});
|
|
195
|
+
req.setTimeout(REQUEST_TIMEOUT_MS, () => {
|
|
196
|
+
req.destroy(new Error(`Token exchange timed out after ${REQUEST_TIMEOUT_MS / 1000}s`));
|
|
197
|
+
});
|
|
198
|
+
req.on('error', reject);
|
|
199
|
+
req.write(body);
|
|
200
|
+
req.end();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// ── Indirection object (sinon-stubbable) ──────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* The login command calls loginFlowClient.X() so tests can replace properties with stubs.
|
|
206
|
+
*/
|
|
207
|
+
export const loginFlowClient = {
|
|
208
|
+
generatePkce,
|
|
209
|
+
generateNonce,
|
|
210
|
+
generateState,
|
|
211
|
+
findAvailablePort,
|
|
212
|
+
openBrowser,
|
|
213
|
+
listenForCallback: listenForCallback,
|
|
214
|
+
exchangeCodeForTokens,
|
|
215
|
+
};
|
|
216
|
+
//# sourceMappingURL=loginFlow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loginFlow.js","sourceRoot":"","sources":["../../../src/services/auth/loginFlow.ts"],"names":[],"mappings":"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"}
|
|
@@ -62,6 +62,8 @@ export interface ProjectValidationResult {
|
|
|
62
62
|
uncovered_test_cases: string[];
|
|
63
63
|
};
|
|
64
64
|
saved_to: string | null;
|
|
65
|
+
/** Directories within plans/ that are missing a .planitem file and will be silently ignored by the Provar runner. */
|
|
66
|
+
plan_integrity_warnings?: string[];
|
|
65
67
|
/** Set when save_results was requested but the write failed (disk full, permissions, etc.). */
|
|
66
68
|
save_error?: string;
|
|
67
69
|
}
|
|
@@ -88,11 +90,12 @@ export declare function readProjectContext(projectPath: string): {
|
|
|
88
90
|
context: ProjectContext;
|
|
89
91
|
};
|
|
90
92
|
export declare function resolveTestInstance(instancePath: string, projectPath: string): TestCaseInput | null;
|
|
91
|
-
export declare function readSuiteDirectory(dirPath: string, name: string, projectPath: string, depth?: number, coveredPaths?: Set<string>, idMap?: Map<string, string
|
|
92
|
-
export declare function readPlanDirectory(planPath: string, name: string, projectPath: string, coveredPaths?: Set<string>, idMap?: Map<string, string
|
|
93
|
+
export declare function readSuiteDirectory(dirPath: string, name: string, projectPath: string, depth?: number, coveredPaths?: Set<string>, idMap?: Map<string, string>, planIntegrityWarnings?: string[]): TestSuiteInput;
|
|
94
|
+
export declare function readPlanDirectory(planPath: string, name: string, projectPath: string, coveredPaths?: Set<string>, idMap?: Map<string, string>, planIntegrityWarnings?: string[]): TestPlanInput;
|
|
93
95
|
export declare function readPlansDir(projectPath: string): {
|
|
94
96
|
plans: TestPlanInput[];
|
|
95
97
|
coveredPaths: Set<string>;
|
|
98
|
+
planIntegrityWarnings: string[];
|
|
96
99
|
};
|
|
97
100
|
/**
|
|
98
101
|
* Collects all .testcase file basenames (without extension) found under tests/.
|
|
@@ -70,7 +70,9 @@ export function resolveProjectRoot(givenPath) {
|
|
|
70
70
|
candidates.push(sub);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
catch {
|
|
73
|
+
catch {
|
|
74
|
+
/* skip */
|
|
75
|
+
}
|
|
74
76
|
if (candidates.length === 1)
|
|
75
77
|
return { root: candidates[0], candidates: [] };
|
|
76
78
|
return { root: givenPath, candidates }; // caller handles 0 or multiple
|
|
@@ -127,7 +129,9 @@ export function readProjectContext(projectPath) {
|
|
|
127
129
|
unencryptedSecretCount++;
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
|
-
catch {
|
|
132
|
+
catch {
|
|
133
|
+
/* skip */
|
|
134
|
+
}
|
|
131
135
|
}
|
|
132
136
|
return {
|
|
133
137
|
projectName,
|
|
@@ -158,16 +162,21 @@ function resolveTestInstanceFull(instancePath, projectPath) {
|
|
|
158
162
|
// Bounds check: only read test case files within the project directory
|
|
159
163
|
const tcInBounds = tcFullPath === projResolved || tcFullPath.startsWith(projResolved + path.sep);
|
|
160
164
|
// Derive name from the bounds-checked resolved path to prevent injection via crafted testCasePath
|
|
161
|
-
const tcName = tcInBounds
|
|
162
|
-
? path.basename(tcFullPath, '.testcase')
|
|
163
|
-
: path.basename(testCasePath, '.testcase');
|
|
165
|
+
const tcName = tcInBounds ? path.basename(tcFullPath, '.testcase') : path.basename(testCasePath, '.testcase');
|
|
164
166
|
if (tcInBounds && fs.existsSync(tcFullPath)) {
|
|
165
167
|
try {
|
|
166
168
|
xml_content = fs.readFileSync(tcFullPath, 'utf-8');
|
|
167
169
|
}
|
|
168
|
-
catch {
|
|
170
|
+
catch {
|
|
171
|
+
/* xml_content stays undefined */
|
|
172
|
+
}
|
|
169
173
|
}
|
|
170
|
-
|
|
174
|
+
// Only expose testCasePath/testCaseId when in-bounds — out-of-bounds paths must not affect coverage totals
|
|
175
|
+
return {
|
|
176
|
+
testCase: { name: tcName, xml_content },
|
|
177
|
+
testCasePath: tcInBounds ? testCasePath : null,
|
|
178
|
+
testCaseId: tcInBounds ? testCaseId : null,
|
|
179
|
+
};
|
|
171
180
|
}
|
|
172
181
|
catch {
|
|
173
182
|
return { testCase: null, testCasePath: null, testCaseId: null };
|
|
@@ -188,7 +197,7 @@ function accumulateCoveredPath(testCasePath, testCaseId, coveredPaths, idMap) {
|
|
|
188
197
|
coveredPaths.add(resolved);
|
|
189
198
|
}
|
|
190
199
|
}
|
|
191
|
-
export function readSuiteDirectory(dirPath, name, projectPath, depth = 0, coveredPaths, idMap) {
|
|
200
|
+
export function readSuiteDirectory(dirPath, name, projectPath, depth = 0, coveredPaths, idMap, planIntegrityWarnings) {
|
|
192
201
|
const testCases = [];
|
|
193
202
|
const testSuites = [];
|
|
194
203
|
if (depth > MAX_SUITE_DEPTH)
|
|
@@ -200,7 +209,13 @@ export function readSuiteDirectory(dirPath, name, projectPath, depth = 0, covere
|
|
|
200
209
|
continue;
|
|
201
210
|
const fullPath = path.join(dirPath, entry.name);
|
|
202
211
|
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
203
|
-
|
|
212
|
+
const subHasPlanItem = fs.existsSync(path.join(fullPath, '.planitem'));
|
|
213
|
+
if (planIntegrityWarnings && !subHasPlanItem) {
|
|
214
|
+
const rel = path.relative(projectPath, fullPath).replace(/\\/g, '/');
|
|
215
|
+
planIntegrityWarnings.push(`${rel}/ is missing a .planitem file — test instances in this suite will be invisible to the runner.`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
testSuites.push(readSuiteDirectory(fullPath, entry.name, projectPath, depth + 1, subHasPlanItem ? coveredPaths : undefined, subHasPlanItem ? idMap : undefined, planIntegrityWarnings));
|
|
204
219
|
}
|
|
205
220
|
else if (entry.name.endsWith('.testinstance')) {
|
|
206
221
|
const { testCase, testCasePath, testCaseId } = resolveTestInstanceFull(fullPath, projectPath);
|
|
@@ -211,10 +226,12 @@ export function readSuiteDirectory(dirPath, name, projectPath, depth = 0, covere
|
|
|
211
226
|
}
|
|
212
227
|
}
|
|
213
228
|
}
|
|
214
|
-
catch {
|
|
229
|
+
catch {
|
|
230
|
+
/* skip */
|
|
231
|
+
}
|
|
215
232
|
return { name, test_cases: testCases, test_suites: testSuites };
|
|
216
233
|
}
|
|
217
|
-
export function readPlanDirectory(planPath, name, projectPath, coveredPaths, idMap) {
|
|
234
|
+
export function readPlanDirectory(planPath, name, projectPath, coveredPaths, idMap, planIntegrityWarnings) {
|
|
218
235
|
const testCases = [];
|
|
219
236
|
const testSuites = [];
|
|
220
237
|
try {
|
|
@@ -224,7 +241,13 @@ export function readPlanDirectory(planPath, name, projectPath, coveredPaths, idM
|
|
|
224
241
|
continue;
|
|
225
242
|
const fullPath = path.join(planPath, entry.name);
|
|
226
243
|
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
227
|
-
|
|
244
|
+
const suiteHasPlanItem = fs.existsSync(path.join(fullPath, '.planitem'));
|
|
245
|
+
if (planIntegrityWarnings && !suiteHasPlanItem) {
|
|
246
|
+
const rel = path.relative(projectPath, fullPath).replace(/\\/g, '/');
|
|
247
|
+
planIntegrityWarnings.push(`${rel}/ is missing a .planitem file — test instances in this suite will be invisible to the runner.`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
testSuites.push(readSuiteDirectory(fullPath, entry.name, projectPath, 0, suiteHasPlanItem ? coveredPaths : undefined, suiteHasPlanItem ? idMap : undefined, planIntegrityWarnings));
|
|
228
251
|
}
|
|
229
252
|
else if (entry.name.endsWith('.testinstance')) {
|
|
230
253
|
const { testCase, testCasePath, testCaseId } = resolveTestInstanceFull(fullPath, projectPath);
|
|
@@ -235,14 +258,17 @@ export function readPlanDirectory(planPath, name, projectPath, coveredPaths, idM
|
|
|
235
258
|
}
|
|
236
259
|
}
|
|
237
260
|
}
|
|
238
|
-
catch {
|
|
261
|
+
catch {
|
|
262
|
+
/* skip */
|
|
263
|
+
}
|
|
239
264
|
return { name, test_cases: testCases, test_suites: testSuites };
|
|
240
265
|
}
|
|
241
266
|
export function readPlansDir(projectPath) {
|
|
242
267
|
const plansDir = path.join(projectPath, 'plans');
|
|
243
268
|
const coveredPaths = new Set();
|
|
269
|
+
const planIntegrityWarnings = [];
|
|
244
270
|
if (!fs.existsSync(plansDir))
|
|
245
|
-
return { plans: [], coveredPaths };
|
|
271
|
+
return { plans: [], coveredPaths, planIntegrityWarnings };
|
|
246
272
|
// Build UUID→path map once so the plan walk can resolve testCaseId fallbacks
|
|
247
273
|
// without a separate pass over the tests/ directory later.
|
|
248
274
|
const idMap = buildTestCaseIdMap(projectPath);
|
|
@@ -253,11 +279,17 @@ export function readPlansDir(projectPath) {
|
|
|
253
279
|
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
254
280
|
continue;
|
|
255
281
|
const planPath = path.join(plansDir, entry.name);
|
|
256
|
-
|
|
282
|
+
const planHasPlanItem = fs.existsSync(path.join(planPath, '.planitem'));
|
|
283
|
+
if (!planHasPlanItem) {
|
|
284
|
+
planIntegrityWarnings.push(`plans/${entry.name}/ is missing a .planitem file — this plan will not be recognised by the Provar runner.`);
|
|
285
|
+
}
|
|
286
|
+
plans.push(readPlanDirectory(planPath, entry.name, projectPath, planHasPlanItem ? coveredPaths : undefined, planHasPlanItem ? idMap : undefined, planIntegrityWarnings));
|
|
257
287
|
}
|
|
258
288
|
}
|
|
259
|
-
catch {
|
|
260
|
-
|
|
289
|
+
catch {
|
|
290
|
+
/* skip */
|
|
291
|
+
}
|
|
292
|
+
return { plans, coveredPaths, planIntegrityWarnings };
|
|
261
293
|
}
|
|
262
294
|
/**
|
|
263
295
|
* Builds a map of testcase UUID (registryId / id / guid) → project-relative path.
|
|
@@ -284,15 +316,20 @@ function buildTestCaseIdMap(projectPath) {
|
|
|
284
316
|
const rel = path.relative(projectPath, fullPath).replace(/\\/g, '/');
|
|
285
317
|
for (const attr of ['registryId', 'id', 'guid']) {
|
|
286
318
|
const m = content.match(new RegExp(`${attr}=["']([^"']+)["']`));
|
|
287
|
-
|
|
319
|
+
// Skip id="1" — new generator emits this literal and it is not UUID-unique
|
|
320
|
+
if (m?.[1] && m[1] !== '1' && !idMap.has(m[1]))
|
|
288
321
|
idMap.set(m[1], rel);
|
|
289
322
|
}
|
|
290
323
|
}
|
|
291
|
-
catch {
|
|
324
|
+
catch {
|
|
325
|
+
/* skip */
|
|
326
|
+
}
|
|
292
327
|
}
|
|
293
328
|
}
|
|
294
329
|
}
|
|
295
|
-
catch {
|
|
330
|
+
catch {
|
|
331
|
+
/* skip */
|
|
332
|
+
}
|
|
296
333
|
}
|
|
297
334
|
walk(testsDir);
|
|
298
335
|
return idMap;
|
|
@@ -320,7 +357,9 @@ export function collectAllTestCaseNames(projectPath) {
|
|
|
320
357
|
names.push(path.basename(entry.name, '.testcase'));
|
|
321
358
|
}
|
|
322
359
|
}
|
|
323
|
-
catch {
|
|
360
|
+
catch {
|
|
361
|
+
/* skip */
|
|
362
|
+
}
|
|
324
363
|
}
|
|
325
364
|
walk(testsDir);
|
|
326
365
|
return names;
|
|
@@ -360,11 +399,15 @@ export function collectCoveredPathsFromDisk(projectPath) {
|
|
|
360
399
|
covered.add(resolvedPath);
|
|
361
400
|
}
|
|
362
401
|
}
|
|
363
|
-
catch {
|
|
402
|
+
catch {
|
|
403
|
+
/* skip */
|
|
404
|
+
}
|
|
364
405
|
}
|
|
365
406
|
}
|
|
366
407
|
}
|
|
367
|
-
catch {
|
|
408
|
+
catch {
|
|
409
|
+
/* skip */
|
|
410
|
+
}
|
|
368
411
|
}
|
|
369
412
|
walk(plansDir);
|
|
370
413
|
return covered;
|
|
@@ -390,7 +433,9 @@ export function findUncoveredTestCases(projectPath, coveredPaths) {
|
|
|
390
433
|
}
|
|
391
434
|
}
|
|
392
435
|
}
|
|
393
|
-
catch {
|
|
436
|
+
catch {
|
|
437
|
+
/* skip */
|
|
438
|
+
}
|
|
394
439
|
}
|
|
395
440
|
walk(testsDir);
|
|
396
441
|
return uncovered.sort();
|
|
@@ -426,7 +471,7 @@ function tcIssuesToQhViolations(tc) {
|
|
|
426
471
|
appliesTo: issue.applies_to,
|
|
427
472
|
});
|
|
428
473
|
}
|
|
429
|
-
for (const bp of
|
|
474
|
+
for (const bp of tc.best_practices_violations ?? []) {
|
|
430
475
|
violations.push({
|
|
431
476
|
number: num++,
|
|
432
477
|
ruleId: bp.rule_id,
|
|
@@ -530,7 +575,13 @@ export function buildQhReport(result, projectName) {
|
|
|
530
575
|
return {
|
|
531
576
|
reportInfo: {
|
|
532
577
|
name: `VR-LOCAL-${now.toISOString().replace(/[:.]/g, '-').slice(0, 19)}`,
|
|
533
|
-
generatedAt: now.toLocaleDateString('en-US', {
|
|
578
|
+
generatedAt: now.toLocaleDateString('en-US', {
|
|
579
|
+
year: 'numeric',
|
|
580
|
+
month: 'short',
|
|
581
|
+
day: 'numeric',
|
|
582
|
+
hour: '2-digit',
|
|
583
|
+
minute: '2-digit',
|
|
584
|
+
}),
|
|
534
585
|
exportedAt: now.toISOString(),
|
|
535
586
|
source: 'provar-mcp-local',
|
|
536
587
|
},
|
|
@@ -551,9 +602,7 @@ export function buildQhReport(result, projectName) {
|
|
|
551
602
|
};
|
|
552
603
|
}
|
|
553
604
|
export function saveResults(projectPath, resultsDir, report, projectName) {
|
|
554
|
-
const targetDir = resultsDir
|
|
555
|
-
? path.resolve(resultsDir)
|
|
556
|
-
: path.join(projectPath, 'provardx', 'validation');
|
|
605
|
+
const targetDir = resultsDir ? path.resolve(resultsDir) : path.join(projectPath, 'provardx', 'validation');
|
|
557
606
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
558
607
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
559
608
|
const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
|
|
@@ -582,7 +631,9 @@ export function validateProjectFromPath(options) {
|
|
|
582
631
|
}
|
|
583
632
|
const { root: projectRoot, candidates } = resolveProjectRoot(resolved);
|
|
584
633
|
if (candidates.length > 1) {
|
|
585
|
-
throw new ProjectValidationError('AMBIGUOUS_PROJECT', `Multiple Provar projects found under "${resolved}". Specify the exact project directory: ${candidates
|
|
634
|
+
throw new ProjectValidationError('AMBIGUOUS_PROJECT', `Multiple Provar projects found under "${resolved}". Specify the exact project directory: ${candidates
|
|
635
|
+
.map((c) => path.basename(c))
|
|
636
|
+
.join(', ')}`);
|
|
586
637
|
}
|
|
587
638
|
if (!fs.existsSync(path.join(projectRoot, '.testproject'))) {
|
|
588
639
|
throw new ProjectValidationError('NOT_A_PROJECT', `No Provar project found at "${projectRoot}". Ensure the path points to a directory containing a .testproject file.`);
|
|
@@ -592,7 +643,7 @@ export function validateProjectFromPath(options) {
|
|
|
592
643
|
const { projectName, context } = readProjectContext(projectRoot);
|
|
593
644
|
// 2. Read plan hierarchy from plans/ directory; covered paths are computed
|
|
594
645
|
// as a byproduct of the walk — no second traversal needed.
|
|
595
|
-
const { plans: testPlans, coveredPaths } = readPlansDir(projectRoot);
|
|
646
|
+
const { plans: testPlans, coveredPaths, planIntegrityWarnings } = readPlansDir(projectRoot);
|
|
596
647
|
// 3. Validate
|
|
597
648
|
const input = {
|
|
598
649
|
name: projectName,
|
|
@@ -672,6 +723,7 @@ export function validateProjectFromPath(options) {
|
|
|
672
723
|
uncovered_test_cases: uncoveredTestCases,
|
|
673
724
|
},
|
|
674
725
|
saved_to: savedTo,
|
|
726
|
+
...(planIntegrityWarnings.length > 0 ? { plan_integrity_warnings: planIntegrityWarnings } : {}),
|
|
675
727
|
save_error: saveError,
|
|
676
728
|
};
|
|
677
729
|
}
|