@qulib/core 0.2.0 → 0.2.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.
@@ -23,13 +23,13 @@ export declare const FrameworkRecommendationSchema: z.ZodObject<{
23
23
  reason: z.ZodString;
24
24
  confidence: z.ZodEnum<["high", "medium", "low"]>;
25
25
  }, "strip", z.ZodTypeAny, {
26
+ confidence: "high" | "medium" | "low";
26
27
  reason: string;
27
28
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
28
- confidence: "high" | "medium" | "low";
29
29
  }, {
30
+ confidence: "high" | "medium" | "low";
30
31
  reason: string;
31
32
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
32
- confidence: "high" | "medium" | "low";
33
33
  }>;
34
34
  export declare const TestStepSchema: z.ZodObject<{
35
35
  action: z.ZodEnum<["navigate", "click", "type", "assert-visible", "assert-hidden", "assert-text", "assert-disabled", "assert-count", "wait", "api-call"]>;
@@ -75,13 +75,13 @@ export declare const NeutralScenarioSchema: z.ZodObject<{
75
75
  reason: z.ZodString;
76
76
  confidence: z.ZodEnum<["high", "medium", "low"]>;
77
77
  }, "strip", z.ZodTypeAny, {
78
+ confidence: "high" | "medium" | "low";
78
79
  reason: string;
79
80
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
80
- confidence: "high" | "medium" | "low";
81
81
  }, {
82
+ confidence: "high" | "medium" | "low";
82
83
  reason: string;
83
84
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
84
- confidence: "high" | "medium" | "low";
85
85
  }>, "many">;
86
86
  sourceGapIds: z.ZodArray<z.ZodString, "many">;
87
87
  }, "strip", z.ZodTypeAny, {
@@ -97,9 +97,9 @@ export declare const NeutralScenarioSchema: z.ZodObject<{
97
97
  }[];
98
98
  tags: string[];
99
99
  recommendations: {
100
+ confidence: "high" | "medium" | "low";
100
101
  reason: string;
101
102
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
102
- confidence: "high" | "medium" | "low";
103
103
  }[];
104
104
  sourceGapIds: string[];
105
105
  targetComponent?: string | undefined;
@@ -116,9 +116,9 @@ export declare const NeutralScenarioSchema: z.ZodObject<{
116
116
  }[];
117
117
  tags: string[];
118
118
  recommendations: {
119
+ confidence: "high" | "medium" | "low";
119
120
  reason: string;
120
121
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
121
- confidence: "high" | "medium" | "low";
122
122
  }[];
123
123
  sourceGapIds: string[];
124
124
  targetComponent?: string | undefined;
@@ -132,17 +132,17 @@ export declare const GeneratedTestSchema: z.ZodObject<{
132
132
  outputPath: z.ZodString;
133
133
  }, "strip", z.ZodTypeAny, {
134
134
  code: string;
135
+ source: "llm" | "template";
135
136
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
136
137
  scenarioId: string;
137
138
  filename: string;
138
- source: "llm" | "template";
139
139
  outputPath: string;
140
140
  }, {
141
141
  code: string;
142
+ source: "llm" | "template";
142
143
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
143
144
  scenarioId: string;
144
145
  filename: string;
145
- source: "llm" | "template";
146
146
  outputPath: string;
147
147
  }>;
148
148
  export declare const GapAnalysisSchema: z.ZodObject<{
@@ -199,13 +199,13 @@ export declare const GapAnalysisSchema: z.ZodObject<{
199
199
  reason: z.ZodString;
200
200
  confidence: z.ZodEnum<["high", "medium", "low"]>;
201
201
  }, "strip", z.ZodTypeAny, {
202
+ confidence: "high" | "medium" | "low";
202
203
  reason: string;
203
204
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
204
- confidence: "high" | "medium" | "low";
205
205
  }, {
206
+ confidence: "high" | "medium" | "low";
206
207
  reason: string;
207
208
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
208
- confidence: "high" | "medium" | "low";
209
209
  }>, "many">;
210
210
  sourceGapIds: z.ZodArray<z.ZodString, "many">;
211
211
  }, "strip", z.ZodTypeAny, {
@@ -221,9 +221,9 @@ export declare const GapAnalysisSchema: z.ZodObject<{
221
221
  }[];
222
222
  tags: string[];
223
223
  recommendations: {
224
+ confidence: "high" | "medium" | "low";
224
225
  reason: string;
225
226
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
226
- confidence: "high" | "medium" | "low";
227
227
  }[];
228
228
  sourceGapIds: string[];
229
229
  targetComponent?: string | undefined;
@@ -240,9 +240,9 @@ export declare const GapAnalysisSchema: z.ZodObject<{
240
240
  }[];
241
241
  tags: string[];
242
242
  recommendations: {
243
+ confidence: "high" | "medium" | "low";
243
244
  reason: string;
244
245
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
245
- confidence: "high" | "medium" | "low";
246
246
  }[];
247
247
  sourceGapIds: string[];
248
248
  targetComponent?: string | undefined;
@@ -256,17 +256,17 @@ export declare const GapAnalysisSchema: z.ZodObject<{
256
256
  outputPath: z.ZodString;
257
257
  }, "strip", z.ZodTypeAny, {
258
258
  code: string;
259
+ source: "llm" | "template";
259
260
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
260
261
  scenarioId: string;
261
262
  filename: string;
262
- source: "llm" | "template";
263
263
  outputPath: string;
264
264
  }, {
265
265
  code: string;
266
+ source: "llm" | "template";
266
267
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
267
268
  scenarioId: string;
268
269
  filename: string;
269
- source: "llm" | "template";
270
270
  outputPath: string;
271
271
  }>, "many">;
272
272
  }, "strip", z.ZodTypeAny, {
@@ -295,19 +295,19 @@ export declare const GapAnalysisSchema: z.ZodObject<{
295
295
  }[];
296
296
  tags: string[];
297
297
  recommendations: {
298
+ confidence: "high" | "medium" | "low";
298
299
  reason: string;
299
300
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
300
- confidence: "high" | "medium" | "low";
301
301
  }[];
302
302
  sourceGapIds: string[];
303
303
  targetComponent?: string | undefined;
304
304
  }[];
305
305
  generatedTests: {
306
306
  code: string;
307
+ source: "llm" | "template";
307
308
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
308
309
  scenarioId: string;
309
310
  filename: string;
310
- source: "llm" | "template";
311
311
  outputPath: string;
312
312
  }[];
313
313
  coverageWarning?: "auth-required" | "budget-exceeded" | "low-coverage" | "navigation-failures" | undefined;
@@ -337,19 +337,19 @@ export declare const GapAnalysisSchema: z.ZodObject<{
337
337
  }[];
338
338
  tags: string[];
339
339
  recommendations: {
340
+ confidence: "high" | "medium" | "low";
340
341
  reason: string;
341
342
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
342
- confidence: "high" | "medium" | "low";
343
343
  }[];
344
344
  sourceGapIds: string[];
345
345
  targetComponent?: string | undefined;
346
346
  }[];
347
347
  generatedTests: {
348
348
  code: string;
349
+ source: "llm" | "template";
349
350
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
350
351
  scenarioId: string;
351
352
  filename: string;
352
- source: "llm" | "template";
353
353
  outputPath: string;
354
354
  }[];
355
355
  coverageWarning?: "auth-required" | "budget-exceeded" | "low-coverage" | "navigation-failures" | undefined;
@@ -1,4 +1,4 @@
1
- export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, type ExplorerType, type AdapterType, type FormLoginAuthConfig, type StorageStateAuthConfig, type AuthConfig, type HarnessConfig, type DetectedAuth, } from './config.schema.js';
1
+ export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, AuthPathRequirementsSchema, AuthPathSchema, AuthExplorationSchema, type ExplorerType, type AdapterType, type FormLoginAuthConfig, type StorageStateAuthConfig, type AuthConfig, type HarnessConfig, type DetectedAuth, type AuthPathRequirements, type AuthPath, type AuthExploration, } from './config.schema.js';
2
2
  export { DecisionLogEntrySchema, type DecisionLogEntry, } from './decision-log.schema.js';
3
3
  export { RouteInventorySchema, RouteSchema, A11yViolationSchema, BrokenLinkSchema, type RouteInventory, type Route, } from './route-inventory.schema.js';
4
4
  export { GapAnalysisSchema, GapSchema, NeutralScenarioSchema, GeneratedTestSchema, TestStepSchema, FrameworkRecommendationSchema, type GapAnalysis, type Gap, type NeutralScenario, type GeneratedTest, type TestStep, type FrameworkRecommendation, } from './gap-analysis.schema.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,KAAK,GACX,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EACd,6BAA6B,EAC7B,KAAK,WAAW,EAChB,KAAK,GAAG,EACR,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,kBAAkB,EAClB,KAAK,YAAY,GAClB,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,QAAQ,EACb,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,KAAK,GACX,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EACd,6BAA6B,EAC7B,KAAK,WAAW,EAChB,KAAK,GAAG,EACR,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,kBAAkB,EAClB,KAAK,YAAY,GAClB,MAAM,2BAA2B,CAAC"}
@@ -1,4 +1,4 @@
1
- export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, } from './config.schema.js';
1
+ export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, AuthPathRequirementsSchema, AuthPathSchema, AuthExplorationSchema, } from './config.schema.js';
2
2
  export { DecisionLogEntrySchema, } from './decision-log.schema.js';
3
3
  export { RouteInventorySchema, RouteSchema, A11yViolationSchema, BrokenLinkSchema, } from './route-inventory.schema.js';
4
4
  export { GapAnalysisSchema, GapSchema, NeutralScenarioSchema, GeneratedTestSchema, TestStepSchema, FrameworkRecommendationSchema, } from './gap-analysis.schema.js';
@@ -5,12 +5,12 @@ export declare const RepoRouteSchema: z.ZodObject<{
5
5
  method: z.ZodEnum<["GET", "POST", "PUT", "DELETE", "PATCH", "unknown"]>;
6
6
  }, "strip", z.ZodTypeAny, {
7
7
  path: string;
8
- file: string;
9
8
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
9
+ file: string;
10
10
  }, {
11
11
  path: string;
12
- file: string;
13
12
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
13
+ file: string;
14
14
  }>;
15
15
  export declare const TestFileSchema: z.ZodObject<{
16
16
  file: z.ZodString;
@@ -62,12 +62,12 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
62
62
  method: z.ZodEnum<["GET", "POST", "PUT", "DELETE", "PATCH", "unknown"]>;
63
63
  }, "strip", z.ZodTypeAny, {
64
64
  path: string;
65
- file: string;
66
65
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
66
+ file: string;
67
67
  }, {
68
68
  path: string;
69
- file: string;
70
69
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
70
+ file: string;
71
71
  }>, "many">;
72
72
  testFiles: z.ZodArray<z.ZodObject<{
73
73
  file: z.ZodString;
@@ -115,8 +115,8 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
115
115
  scannedAt: string;
116
116
  routes: {
117
117
  path: string;
118
- file: string;
119
118
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
119
+ file: string;
120
120
  }[];
121
121
  repoPath: string;
122
122
  testFiles: {
@@ -139,8 +139,8 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
139
139
  scannedAt: string;
140
140
  routes: {
141
141
  path: string;
142
- file: string;
143
142
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
143
+ file: string;
144
144
  }[];
145
145
  repoPath: string;
146
146
  testFiles: {
@@ -1 +1 @@
1
- {"version":3,"file":"auth-detector.d.ts","sourceRoot":"","sources":["../../src/tools/auth-detector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAmDhE,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,CAgGtF"}
1
+ {"version":3,"file":"auth-detector.d.ts","sourceRoot":"","sources":["../../src/tools/auth-detector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AA4DhE,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,CAkGtF"}
@@ -1,4 +1,12 @@
1
- import { chromium } from '@playwright/test';
1
+ import { launchBrowser } from './browser.js';
2
+ async function waitNetworkIdleBestEffort(page) {
3
+ try {
4
+ await page.waitForLoadState('networkidle', { timeout: 5000 });
5
+ }
6
+ catch {
7
+ // best-effort — analytics or polling can prevent networkidle
8
+ }
9
+ }
2
10
  const OAUTH_PROVIDERS = [
3
11
  { provider: 'github', patterns: [/github/i, /sign in with github/i] },
4
12
  {
@@ -43,11 +51,12 @@ async function firstTextInputNameForLogin(page) {
43
51
  return null;
44
52
  }
45
53
  export async function detectAuth(url, timeoutMs = 15000) {
46
- const browser = await chromium.launch({ headless: true });
54
+ const browser = await launchBrowser();
47
55
  try {
48
56
  const context = await browser.newContext();
49
57
  const page = await context.newPage();
50
58
  await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
59
+ await waitNetworkIdleBestEffort(page);
51
60
  let loginUrl = url;
52
61
  const looksLikeLoginPage = /login|sign[- ]?in|auth/i.test(page.url()) ||
53
62
  (await page.locator('input[type="password"]').count()) > 0;
@@ -58,6 +67,7 @@ export async function detectAuth(url, timeoutMs = 15000) {
58
67
  if (href) {
59
68
  loginUrl = new URL(href, url).toString();
60
69
  await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
70
+ await waitNetworkIdleBestEffort(page);
61
71
  }
62
72
  }
63
73
  }
@@ -0,0 +1,3 @@
1
+ import { type AuthExploration } from '../schemas/config.schema.js';
2
+ export declare function exploreAuth(url: string, timeoutMs?: number): Promise<AuthExploration>;
3
+ //# sourceMappingURL=auth-explorer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-explorer.d.ts","sourceRoot":"","sources":["../../src/tools/auth-explorer.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,6BAA6B,CAAC;AA6MrC,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,eAAe,CAAC,CA2J1F"}
@@ -0,0 +1,324 @@
1
+ import { AuthExplorationSchema, } from '../schemas/config.schema.js';
2
+ import { launchBrowser } from './browser.js';
3
+ import { BUILT_IN_OAUTH_PROVIDERS } from './oauth-providers.js';
4
+ import { loadUserProviders } from './user-providers.js';
5
+ const MAGIC_LINK_PATTERNS = [
6
+ /email me a (sign[- ]?in )?link/i,
7
+ /sign in with email/i,
8
+ /passwordless/i,
9
+ /we'll send you a link/i,
10
+ ];
11
+ async function waitNetworkIdleBestEffort(page) {
12
+ try {
13
+ await page.waitForLoadState('networkidle', { timeout: 5000 });
14
+ }
15
+ catch {
16
+ // best-effort
17
+ }
18
+ }
19
+ function textLooksLikeOAuthIdpButton(text) {
20
+ const t = text.trim();
21
+ if (t.length === 0 || t.length > 120) {
22
+ return false;
23
+ }
24
+ return (/\b(sign in with|log in with|continue with|sign up with)\b/i.test(t) ||
25
+ /^(github|google|microsoft|apple)$/i.test(t));
26
+ }
27
+ function slugifyLabel(text) {
28
+ const s = text
29
+ .trim()
30
+ .toLowerCase()
31
+ .replace(/\s+/g, '-')
32
+ .replace(/[^a-z0-9-]/g, '')
33
+ .slice(0, 48);
34
+ return s.length > 0 ? s : 'unknown';
35
+ }
36
+ function onLoginishPage(url) {
37
+ return /login|sign[- ]?in|auth|sso|oauth/i.test(new URL(url).pathname + new URL(url).hostname);
38
+ }
39
+ function isHeuristicUnknownSso(text, loginish) {
40
+ const t = text.trim();
41
+ if (!loginish || t.length < 3 || t.length > 80) {
42
+ return false;
43
+ }
44
+ if (/^(submit|cancel|back|close|next|skip|help|faq)$/i.test(t)) {
45
+ return false;
46
+ }
47
+ if (/\b(sign in with|log in with|continue with)\b/i.test(t)) {
48
+ return true;
49
+ }
50
+ if (/\b(sync|sso|portal|workspace|federation)\b/i.test(t)) {
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+ function storageRequirement() {
56
+ return {
57
+ method: 'storage-state',
58
+ instruction: 'OAuth and most SSO flows cannot be scripted. Run `qulib auth init --base-url <app-url>` on this machine, then pass the saved storage state JSON to `analyze` or MCP `analyze_app` as `auth: { type: "storage-state", path: "..." }`.',
59
+ };
60
+ }
61
+ async function collectVisibleControlTexts(page) {
62
+ return page.evaluate(() => {
63
+ const seen = new Set();
64
+ const out = [];
65
+ const nodes = document.querySelectorAll('button, a[href], [role="button"]');
66
+ for (const el of nodes) {
67
+ const t = (el.textContent ?? '').trim().replace(/\s+/g, ' ');
68
+ if (!t || t.length > 120) {
69
+ continue;
70
+ }
71
+ const style = window.getComputedStyle(el);
72
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
73
+ continue;
74
+ }
75
+ if (!seen.has(t)) {
76
+ seen.add(t);
77
+ out.push(t);
78
+ }
79
+ }
80
+ return out;
81
+ });
82
+ }
83
+ function buildAllProviders() {
84
+ const builtIn = BUILT_IN_OAUTH_PROVIDERS.map((p) => ({ ...p, source: 'built-in' }));
85
+ const user = loadUserProviders().map((p) => ({ ...p, source: 'user-local' }));
86
+ return [...builtIn, ...user];
87
+ }
88
+ function matchProvider(text, p) {
89
+ return p.patterns.some((re) => re.test(text));
90
+ }
91
+ function oauthConfidence(source, loginish) {
92
+ if (source === 'user-local') {
93
+ return 'high';
94
+ }
95
+ if (source === 'built-in' && loginish) {
96
+ return 'high';
97
+ }
98
+ if (source === 'built-in') {
99
+ return 'medium';
100
+ }
101
+ return 'low';
102
+ }
103
+ async function buildFormPaths(page) {
104
+ const passwordCount = await page.locator('input[type="password"]').count();
105
+ if (passwordCount === 0) {
106
+ return [];
107
+ }
108
+ const formType = passwordCount > 1 ? 'form-multi' : 'form-login';
109
+ const fields = await page.evaluate(() => {
110
+ const pwd = document.querySelector('input[type="password"]');
111
+ if (!pwd) {
112
+ return [];
113
+ }
114
+ const form = pwd.closest('form') ?? document.body;
115
+ const out = [];
116
+ const inputs = form.querySelectorAll('input, select, textarea');
117
+ for (const el of inputs) {
118
+ if (!(el instanceof HTMLElement)) {
119
+ continue;
120
+ }
121
+ const tag = el.tagName.toLowerCase();
122
+ if (tag === 'input') {
123
+ const inp = el;
124
+ const t = (inp.type || 'text').toLowerCase();
125
+ if (['hidden', 'submit', 'button', 'image', 'reset'].includes(t)) {
126
+ continue;
127
+ }
128
+ let fieldType = 'text';
129
+ if (t === 'password') {
130
+ fieldType = 'password';
131
+ }
132
+ else if (t === 'email') {
133
+ fieldType = 'email';
134
+ }
135
+ else if (t === 'checkbox') {
136
+ fieldType = 'checkbox';
137
+ }
138
+ const id = inp.id;
139
+ let label = inp.getAttribute('aria-label') ?? inp.placeholder ?? inp.name ?? fieldType;
140
+ if (id) {
141
+ const lab = document.querySelector(`label[for="${CSS.escape(id)}"]`);
142
+ if (lab?.textContent) {
143
+ label = lab.textContent.trim();
144
+ }
145
+ }
146
+ out.push({
147
+ name: inp.name || inp.id || fieldType,
148
+ label: label.slice(0, 120),
149
+ type: fieldType,
150
+ observedOptions: [],
151
+ });
152
+ }
153
+ else if (tag === 'select') {
154
+ const sel = el;
155
+ const opts = Array.from(sel.options).map((o) => o.text.trim()).filter(Boolean);
156
+ out.push({
157
+ name: sel.name || sel.id || 'select',
158
+ label: (sel.getAttribute('aria-label') ?? sel.name ?? 'select').slice(0, 120),
159
+ type: 'select',
160
+ observedOptions: opts.slice(0, 50),
161
+ });
162
+ }
163
+ }
164
+ return out;
165
+ });
166
+ const requirements = fields.length > 0
167
+ ? { method: 'credentials', fields }
168
+ : {
169
+ method: 'unknown',
170
+ instruction: 'A password field exists but field metadata could not be read. Inspect the page in devtools and configure form-login selectors manually, or use `qulib auth init`.',
171
+ };
172
+ return [
173
+ {
174
+ id: formType === 'form-multi' ? 'form-multi' : 'form-login',
175
+ label: formType === 'form-multi' ? 'Multi-field sign-in form' : 'Username / password form',
176
+ type: formType,
177
+ provider: null,
178
+ source: 'heuristic',
179
+ automatable: requirements.method === 'credentials',
180
+ confidence: requirements.method === 'credentials' ? 'medium' : 'low',
181
+ requirements,
182
+ },
183
+ ];
184
+ }
185
+ export async function exploreAuth(url, timeoutMs = 20000) {
186
+ const browser = await launchBrowser();
187
+ try {
188
+ const context = await browser.newContext();
189
+ const page = await context.newPage();
190
+ await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
191
+ await waitNetworkIdleBestEffort(page);
192
+ const loginishAfterFirst = /login|sign[- ]?in|auth/i.test(page.url()) || (await page.locator('input[type="password"]').count()) > 0;
193
+ if (!loginishAfterFirst) {
194
+ const loginLink = page.locator('a').filter({ hasText: /^(log ?in|sign ?in|sign in)$/i }).first();
195
+ if ((await loginLink.count()) > 0) {
196
+ const href = await loginLink.getAttribute('href');
197
+ if (href) {
198
+ const next = new URL(href, url).toString();
199
+ await page.goto(next, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
200
+ await waitNetworkIdleBestEffort(page);
201
+ }
202
+ }
203
+ }
204
+ const finalUrl = page.url();
205
+ const loginish = onLoginishPage(finalUrl) || (await page.locator('input[type="password"]').count()) > 0;
206
+ const allProviders = buildAllProviders();
207
+ const texts = await collectVisibleControlTexts(page);
208
+ const consumed = new Set();
209
+ const authPaths = [];
210
+ const unrecognizedButtons = [];
211
+ for (const rawText of texts) {
212
+ const text = rawText.trim();
213
+ if (!text) {
214
+ continue;
215
+ }
216
+ let matched = null;
217
+ for (const p of allProviders) {
218
+ if (!matchProvider(text, p)) {
219
+ continue;
220
+ }
221
+ if (p.source === 'built-in' && !(textLooksLikeOAuthIdpButton(text) || loginish)) {
222
+ continue;
223
+ }
224
+ matched = { p, gate: textLooksLikeOAuthIdpButton(text) || loginish };
225
+ break;
226
+ }
227
+ if (matched) {
228
+ const { p, gate } = matched;
229
+ const id = `oauth:${p.id}`;
230
+ if (consumed.has(id)) {
231
+ continue;
232
+ }
233
+ consumed.add(id);
234
+ authPaths.push({
235
+ id,
236
+ label: p.label,
237
+ type: 'oauth',
238
+ provider: p.id,
239
+ source: p.source,
240
+ automatable: false,
241
+ confidence: oauthConfidence(p.source, loginish || gate),
242
+ requirements: storageRequirement(),
243
+ });
244
+ continue;
245
+ }
246
+ if (isHeuristicUnknownSso(text, loginish)) {
247
+ const slug = slugifyLabel(text);
248
+ const id = `oauth-unknown:${slug}`;
249
+ if (consumed.has(id)) {
250
+ continue;
251
+ }
252
+ consumed.add(id);
253
+ authPaths.push({
254
+ id,
255
+ label: text.slice(0, 100),
256
+ type: 'oauth-unknown',
257
+ provider: null,
258
+ source: 'heuristic',
259
+ automatable: false,
260
+ confidence: 'low',
261
+ requirements: storageRequirement(),
262
+ });
263
+ const safePattern = text.slice(0, 48).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
264
+ unrecognizedButtons.push({
265
+ label: text.slice(0, 100),
266
+ hint: `If this is your org SSO, register it: qulib auth providers add --id "${slug}" --label "${text.replace(/"/g, '\\"').slice(0, 80)}" --pattern "${safePattern}"`,
267
+ });
268
+ }
269
+ }
270
+ const pageText = await page.locator('body').innerText().catch(() => '');
271
+ if (MAGIC_LINK_PATTERNS.some((re) => re.test(pageText))) {
272
+ authPaths.push({
273
+ id: 'magic-link',
274
+ label: 'Magic link / passwordless',
275
+ type: 'magic-link',
276
+ provider: null,
277
+ source: 'heuristic',
278
+ automatable: false,
279
+ confidence: 'medium',
280
+ requirements: {
281
+ method: 'storage-state',
282
+ instruction: 'Magic-link flows need a human in the loop. Use `qulib auth init --base-url <app-url>` and complete email or provider steps in the opened browser, then reuse the saved storage state for scans.',
283
+ },
284
+ });
285
+ }
286
+ authPaths.push(...(await buildFormPaths(page)));
287
+ const authRequired = authPaths.length > 0;
288
+ let authScope = 'none';
289
+ if (authRequired) {
290
+ if (loginish) {
291
+ authScope = 'site-wide';
292
+ }
293
+ else {
294
+ authScope = /login|signin|auth/i.test(new URL(finalUrl).pathname) ? 'site-wide' : 'optional';
295
+ }
296
+ }
297
+ const suggestedParts = [];
298
+ if (authPaths.some((p) => p.type === 'oauth' || p.type === 'oauth-unknown')) {
299
+ suggestedParts.push('For OAuth or unrecognized SSO buttons, collect a Playwright storage state with `qulib auth init` before calling `analyze_app`.');
300
+ }
301
+ if (authPaths.some((p) => p.type === 'form-login' || p.type === 'form-multi')) {
302
+ suggestedParts.push('For password forms, gather username/password and stable selectors (or use storage state if MFA applies).');
303
+ }
304
+ if (authPaths.some((p) => p.type === 'magic-link')) {
305
+ suggestedParts.push('For magic-link, use `qulib auth init` after the user completes email delivery.');
306
+ }
307
+ if (!authRequired) {
308
+ suggestedParts.push('No sign-in surface detected at this URL; you can run `analyze_app` without auth unless gated deeper in the app.');
309
+ }
310
+ const exploration = {
311
+ url: finalUrl,
312
+ authRequired,
313
+ authScope,
314
+ authPaths,
315
+ observedAt: new Date().toISOString(),
316
+ suggestedAgentBehavior: suggestedParts.join(' '),
317
+ unrecognizedButtons,
318
+ };
319
+ return AuthExplorationSchema.parse(exploration);
320
+ }
321
+ finally {
322
+ await browser.close();
323
+ }
324
+ }
@@ -0,0 +1,3 @@
1
+ import { type Browser } from '@playwright/test';
2
+ export declare function launchBrowser(): Promise<Browser>;
3
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/tools/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE1D,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAYtD"}
@@ -0,0 +1,13 @@
1
+ import { chromium } from '@playwright/test';
2
+ export async function launchBrowser() {
3
+ try {
4
+ return await chromium.launch({ headless: true });
5
+ }
6
+ catch (err) {
7
+ const message = err instanceof Error ? err.message : String(err);
8
+ if (message.includes("Executable doesn't exist") || message.includes('chromium')) {
9
+ throw new Error(`Playwright Chromium browser is not installed. Run:\n\n npx playwright install chromium\n\nThen retry your qulib command. This is a one-time setup step.`);
10
+ }
11
+ throw err;
12
+ }
13
+ }
@@ -0,0 +1,7 @@
1
+ export interface OAuthProvider {
2
+ id: string;
3
+ label: string;
4
+ patterns: RegExp[];
5
+ }
6
+ export declare const BUILT_IN_OAUTH_PROVIDERS: OAuthProvider[];
7
+ //# sourceMappingURL=oauth-providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-providers.d.ts","sourceRoot":"","sources":["../../src/tools/oauth-providers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,wBAAwB,EAAE,aAAa,EAsBnD,CAAC"}