@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.
@@ -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,CAEhE;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"}
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' && path.source === 'heuristic' && !builtInOAuthIds.has(path.id);
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, i) => `${i + 1}. [${g.severity}] ${g.category} at ${g.path}: ${g.reason}`)
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": ["string"]
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;AAsQD,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"}
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: 2000 });
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;AAiNzE,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAgL1B"}
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.0",
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"