@qulib/core 0.5.0 → 0.5.2
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/dist/cli/auth-login-resolve.js +1 -1
- package/dist/cli/auth-login-run.d.ts.map +1 -1
- package/dist/cli/auth-login-run.js +3 -1
- package/dist/llm/context-builder.js +2 -2
- package/dist/tools/auth/detect.d.ts.map +1 -1
- package/dist/tools/auth/detect.js +16 -2
- package/dist/tools/auth/explore.d.ts.map +1 -1
- package/dist/tools/auth/explore.js +52 -0
- package/package.json +2 -2
|
@@ -29,7 +29,7 @@ export function parseCredentialsJsonString(json) {
|
|
|
29
29
|
return out;
|
|
30
30
|
}
|
|
31
31
|
export function resolveFormLoginPath(baseUrl, authOptions, authPathId) {
|
|
32
|
-
const formPaths = (authOptions ?? []).filter((o) => o.type === 'form-login' && o.requirements.method === 'credentials');
|
|
32
|
+
const formPaths = (authOptions ?? []).filter((o) => (o.type === 'form-login' || o.type === 'form-multi') && o.requirements.method === 'credentials');
|
|
33
33
|
if (formPaths.length === 0) {
|
|
34
34
|
throw new Error(`No automatable form-login path detected on ${baseUrl}. Use \`qulib auth init\` for manual login.`);
|
|
35
35
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-login-run.d.ts","sourceRoot":"","sources":["../../src/cli/auth-login-run.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAsB5D,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"auth-login-run.d.ts","sourceRoot":"","sources":["../../src/cli/auth-login-run.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAsB5D,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAMhE;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,QAAQ,CAAC;IACf,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoIhB"}
|
|
@@ -16,7 +16,9 @@ function sleep(ms) {
|
|
|
16
16
|
return new Promise((r) => setTimeout(r, ms));
|
|
17
17
|
}
|
|
18
18
|
export function authPathNeedsClickReveal(path) {
|
|
19
|
-
return path.type === 'form-login'
|
|
19
|
+
return ((path.type === 'form-login' || path.type === 'form-multi') &&
|
|
20
|
+
(path.source === 'heuristic' || path.source === 'user-local') &&
|
|
21
|
+
!builtInOAuthIds.has(path.id));
|
|
20
22
|
}
|
|
21
23
|
export async function runAutomatedAuthLogin(params) {
|
|
22
24
|
const { chromium } = await import('@playwright/test');
|
|
@@ -6,7 +6,7 @@ export function buildGapPrompt(gaps, limit) {
|
|
|
6
6
|
})
|
|
7
7
|
.slice(0, limit);
|
|
8
8
|
const gapList = topGaps
|
|
9
|
-
.map((g
|
|
9
|
+
.map((g) => `- id:${g.id} [${g.severity}] ${g.category} at ${g.path}: ${g.reason}`)
|
|
10
10
|
.join('\n');
|
|
11
11
|
return `You are a QA engineer. Given these quality gaps found in a web application, generate test scenarios.
|
|
12
12
|
|
|
@@ -28,6 +28,6 @@ Each item must match this exact shape:
|
|
|
28
28
|
"recommendations": [
|
|
29
29
|
{ "adapter": "playwright|cypress-e2e|cypress-component|api|accessibility", "reason": "string", "confidence": "high|medium|low" }
|
|
30
30
|
],
|
|
31
|
-
"sourceGapIds": ["
|
|
31
|
+
"sourceGapIds": ["<one or more gap ids from the list, copied exactly as id:xxxx>"]
|
|
32
32
|
}`;
|
|
33
33
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/detect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAY,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAIzE,MAAM,MAAM,yBAAyB,GACjC,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,cAAc,GACd,yBAAyB,GACzB,iBAAiB,GACjB,SAAS,CAAC;AAEd,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,yBAAyB,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/detect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAY,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAIzE,MAAM,MAAM,yBAAyB,GACjC,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,cAAc,GACd,yBAAyB,GACzB,iBAAiB,GACjB,SAAS,CAAC;AAEd,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,yBAAyB,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB;AAuRD,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACpD,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,OAAO,CAAC;CAC9B,GAAG,4BAA4B,CA6B/B;AAOD,wBAAsB,yBAAyB,CAC7C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC,CAgD9C;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,EACf,SAAS,SAAQ,GAChB,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAelD;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,SAAS,SAAQ,GAChB,OAAO,CAAC,4BAA4B,CAAC,CAyDvC;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAqJvB"}
|
|
@@ -49,6 +49,9 @@ async function firstTextInputNameForLogin(page) {
|
|
|
49
49
|
function debugAuth() {
|
|
50
50
|
return process.env.QULIB_DEBUG === '1';
|
|
51
51
|
}
|
|
52
|
+
function isLoginishPath(pathname) {
|
|
53
|
+
return /login|sign[- ]?in|auth|sso|oauth|signin/i.test(pathname);
|
|
54
|
+
}
|
|
52
55
|
function slugify(label) {
|
|
53
56
|
const s = label
|
|
54
57
|
.toLowerCase()
|
|
@@ -151,7 +154,7 @@ function authPathsFromOauthButtons(oauthButtons, loginUrl) {
|
|
|
151
154
|
}
|
|
152
155
|
async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, timeoutMs, progress) {
|
|
153
156
|
const out = [];
|
|
154
|
-
const buttons = page.locator('button');
|
|
157
|
+
const buttons = page.locator('button, [role="button"]');
|
|
155
158
|
const n = await buttons.count();
|
|
156
159
|
const seenLabels = new Set();
|
|
157
160
|
const SUBMIT_RE = /^(sign in|log in|submit|continue|next|cancel|close)$/i;
|
|
@@ -169,6 +172,7 @@ async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, time
|
|
|
169
172
|
seenLabels.add(label);
|
|
170
173
|
candidateAttempts += 1;
|
|
171
174
|
const originBefore = new URL(page.url()).origin;
|
|
175
|
+
const pathBefore = new URL(page.url()).pathname;
|
|
172
176
|
if (debugAuth()) {
|
|
173
177
|
progress?.debug(`detect_auth click-reveal try label="${label.slice(0, 80)}"`);
|
|
174
178
|
}
|
|
@@ -207,8 +211,18 @@ async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, time
|
|
|
207
211
|
await waitNetworkIdleBestEffort(page);
|
|
208
212
|
continue;
|
|
209
213
|
}
|
|
214
|
+
const afterUrl = new URL(page.url());
|
|
215
|
+
if (afterUrl.pathname !== pathBefore && !isLoginishPath(afterUrl.pathname)) {
|
|
216
|
+
if (debugAuth()) {
|
|
217
|
+
progress?.debug(`detect_auth click-reveal skip (CTA navigation to non-login path): ${afterUrl.pathname}`);
|
|
218
|
+
}
|
|
219
|
+
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
220
|
+
await waitNetworkIdleBestEffort(page);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
await waitNetworkIdleBestEffort(page);
|
|
210
224
|
try {
|
|
211
|
-
await page.locator('input[type="password"]:visible').first().waitFor({ state: 'visible', timeout:
|
|
225
|
+
await page.locator('input[type="password"]:visible').first().waitFor({ state: 'visible', timeout: 5000 });
|
|
212
226
|
}
|
|
213
227
|
catch {
|
|
214
228
|
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"explore.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/explore.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"explore.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/explore.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AA2PzE,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAiM1B"}
|
|
@@ -185,6 +185,43 @@ async function buildFormPaths(page) {
|
|
|
185
185
|
},
|
|
186
186
|
];
|
|
187
187
|
}
|
|
188
|
+
async function probeUserLocalProviderClick(page, providerLabel, loginUrl, timeoutMs) {
|
|
189
|
+
const originBefore = new URL(page.url()).origin;
|
|
190
|
+
let clicked = false;
|
|
191
|
+
try {
|
|
192
|
+
await page.getByRole('button', { name: providerLabel, exact: true }).first().click({ timeout: 3000 });
|
|
193
|
+
clicked = true;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
try {
|
|
197
|
+
const escaped = providerLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
198
|
+
await page
|
|
199
|
+
.locator('button, [role="button"]')
|
|
200
|
+
.filter({ hasText: new RegExp(`^\\s*${escaped}\\s*$`, 'i') })
|
|
201
|
+
.first()
|
|
202
|
+
.click({ timeout: 3000 });
|
|
203
|
+
clicked = true;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
/* skip */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!clicked)
|
|
210
|
+
return [];
|
|
211
|
+
try {
|
|
212
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 5000 });
|
|
213
|
+
}
|
|
214
|
+
catch { /* best-effort */ }
|
|
215
|
+
await waitNetworkIdleBestEffort(page);
|
|
216
|
+
if (new URL(page.url()).origin !== originBefore) {
|
|
217
|
+
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
const formPaths = await buildFormPaths(page);
|
|
221
|
+
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
222
|
+
await waitNetworkIdleBestEffort(page);
|
|
223
|
+
return formPaths;
|
|
224
|
+
}
|
|
188
225
|
export async function exploreAuth(url, timeoutMs = 20000, progress) {
|
|
189
226
|
const browser = await launchBrowser();
|
|
190
227
|
try {
|
|
@@ -246,6 +283,21 @@ export async function exploreAuth(url, timeoutMs = 20000, progress) {
|
|
|
246
283
|
continue;
|
|
247
284
|
}
|
|
248
285
|
consumed.add(id);
|
|
286
|
+
if (p.source === 'user-local') {
|
|
287
|
+
const probed = await probeUserLocalProviderClick(page, p.label, finalUrl, timeoutMs);
|
|
288
|
+
if (probed.length > 0) {
|
|
289
|
+
for (const fp of probed) {
|
|
290
|
+
authPaths.push({
|
|
291
|
+
...fp,
|
|
292
|
+
id: p.id,
|
|
293
|
+
label: p.label,
|
|
294
|
+
source: 'user-local',
|
|
295
|
+
});
|
|
296
|
+
progress?.info(`explore_auth path id=${p.id} type=${fp.type} automatable=${fp.automatable} (user-local probe)`);
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
249
301
|
authPaths.push({
|
|
250
302
|
id,
|
|
251
303
|
label: p.label,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qulib/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Qulib — analyze deployed web apps for honest quality gaps (CLI + programmatic API)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tapesh Nagarwal",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"analyze": "tsx src/cli/index.ts analyze",
|
|
49
49
|
"clean": "tsx src/cli/index.ts clean",
|
|
50
50
|
"build": "tsc",
|
|
51
|
-
"test": "node --import tsx/esm --test src/llm/__tests__/cost-intelligence.test.ts src/tools/scoring/__tests__/gaps.test.ts src/tools/auth/__tests__/gaps.test.ts src/tools/auth/__tests__/detect.test.ts src/tools/scoring/__tests__/automation-maturity.test.ts src/harness/__tests__/state-manager.test.ts src/telemetry/__tests__/redact-url.test.ts src/cli/__tests__/auth-login.test.ts src/cli/__tests__/cli-version.test.ts src/__tests__/analyze.storage-state-invalid.test.ts src/__tests__/analyze.fixtures.test.ts",
|
|
51
|
+
"test": "node --import tsx/esm --test src/llm/__tests__/cost-intelligence.test.ts src/llm/__tests__/context-builder.test.ts src/tools/scoring/__tests__/gaps.test.ts src/tools/auth/__tests__/gaps.test.ts src/tools/auth/__tests__/detect.test.ts src/tools/scoring/__tests__/automation-maturity.test.ts src/harness/__tests__/state-manager.test.ts src/telemetry/__tests__/redact-url.test.ts src/cli/__tests__/auth-login.test.ts src/cli/__tests__/cli-version.test.ts src/__tests__/analyze.storage-state-invalid.test.ts src/__tests__/analyze.fixtures.test.ts",
|
|
52
52
|
"test:integration": "node --import tsx/esm --test src/__tests__/analyze.integration.test.ts",
|
|
53
53
|
"smoke": "tsx src/cli/index.ts analyze --url https://example.com --ephemeral",
|
|
54
54
|
"cost-doctor": "tsx src/cli/index.ts cost doctor"
|