@qulib/core 0.1.1 → 0.2.1

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 CHANGED
@@ -8,6 +8,65 @@
8
8
  npm install @qulib/core
9
9
  ```
10
10
 
11
+ ## One-time browser setup
12
+
13
+ Qulib uses Playwright. Install Chromium once on the machine that runs scans:
14
+
15
+ ```bash
16
+ npx playwright install chromium
17
+ ```
18
+
19
+ If browsers are missing, commands fail with a short message pointing you here.
20
+
21
+ ## Scanning authenticated apps
22
+
23
+ Qulib supports three auth modes: anonymous (default), form-login, and storage-state.
24
+
25
+ ### Form login
26
+
27
+ If your app uses a simple username/password form:
28
+
29
+ ```bash
30
+ qulib analyze --url https://app.example.com \
31
+ --auth-form-login \
32
+ --login-url https://app.example.com/login \
33
+ --username you@example.com \
34
+ --password "..." \
35
+ --username-selector "input[name=email]" \
36
+ --password-selector "input[name=password]" \
37
+ --submit-selector "button[type=submit]"
38
+ ```
39
+
40
+ ### OAuth, magic link, SSO, or anything else
41
+
42
+ These can't be automated. Qulib has a helper for this:
43
+
44
+ ```bash
45
+ qulib auth init --base-url https://app.example.com
46
+ ```
47
+
48
+ This opens a real browser. Log in normally (OAuth, magic link, password manager, whatever). Press ENTER in the terminal when you reach a logged-in page. Qulib saves your session to `qulib-storage-state.json`.
49
+
50
+ Then scan with it:
51
+
52
+ ```bash
53
+ qulib analyze --url https://app.example.com --auth-storage-state ./qulib-storage-state.json
54
+ ```
55
+
56
+ The storage state is just a JSON file of cookies and localStorage — keep it private, treat it like a credential.
57
+
58
+ ### Auth detection
59
+
60
+ To check what auth pattern a site uses before configuring anything:
61
+
62
+ ```bash
63
+ qulib detect-auth --url https://app.example.com
64
+ ```
65
+
66
+ Or via MCP:
67
+
68
+ > "Use qulib's detect_auth tool on https://app.example.com — what's the recommended auth setup?"
69
+
11
70
  ## CLI (from npm)
12
71
 
13
72
  ```bash
package/dist/analyze.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { HarnessConfig } from './schemas/config.schema.js';
2
- import type { GapAnalysis } from './schemas/gap-analysis.schema.js';
1
+ import type { HarnessConfig, DetectedAuth } from './schemas/config.schema.js';
2
+ import { type GapAnalysis } from './schemas/gap-analysis.schema.js';
3
3
  import type { RouteInventory } from './schemas/route-inventory.schema.js';
4
4
  import type { RepoAnalysis } from './schemas/repo-analysis.schema.js';
5
5
  import type { DecisionLogEntry } from './schemas/decision-log.schema.js';
@@ -8,6 +8,7 @@ export interface AnalyzeOptions {
8
8
  repoPath?: string;
9
9
  config: HarnessConfig;
10
10
  writeArtifacts?: boolean;
11
+ skipAuthDetection?: boolean;
11
12
  }
12
13
  export interface AnalyzeResult {
13
14
  releaseConfidence: number;
@@ -15,6 +16,7 @@ export interface AnalyzeResult {
15
16
  routeInventory: RouteInventory;
16
17
  repoInventory: RepoAnalysis | null;
17
18
  decisionLog: DecisionLogEntry[];
19
+ detectedAuth?: DetectedAuth;
18
20
  }
19
21
  export declare function analyzeApp(options: AnalyzeOptions): Promise<AnalyzeResult>;
20
22
  //# sourceMappingURL=analyze.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAKzE,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,WAAW,CAAC;IACzB,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE,gBAAgB,EAAE,CAAC;CACjC;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAmBhF"}
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,kCAAkC,CAAC;AACvF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAMzE,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,WAAW,CAAC;IACzB,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA0DhF"}
package/dist/analyze.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { GapAnalysisSchema } from './schemas/gap-analysis.schema.js';
1
2
  import { observe } from './phases/observe.js';
2
3
  import { think } from './phases/think.js';
3
4
  import { act } from './phases/act.js';
5
+ import { detectAuth } from './tools/auth-detector.js';
4
6
  export async function analyzeApp(options) {
5
7
  const writeArtifacts = options.writeArtifacts ?? false;
6
8
  const decisionLog = [];
@@ -8,6 +10,44 @@ export async function analyzeApp(options) {
8
10
  writeArtifacts,
9
11
  decisionMemory: decisionLog,
10
12
  };
13
+ if (!options.config.auth && !options.skipAuthDetection) {
14
+ const detection = await detectAuth(options.url, options.config.timeoutMs);
15
+ if (detection.hasAuth) {
16
+ const gapAnalysis = GapAnalysisSchema.parse({
17
+ analyzedAt: new Date().toISOString(),
18
+ mode: 'auth-required',
19
+ releaseConfidence: 0,
20
+ coveragePagesScanned: 0,
21
+ coverageBudgetExceeded: false,
22
+ coverageWarning: 'auth-required',
23
+ gaps: [],
24
+ scenarios: [],
25
+ generatedTests: [],
26
+ });
27
+ return {
28
+ releaseConfidence: 0,
29
+ gapAnalysis,
30
+ routeInventory: {
31
+ scannedAt: new Date().toISOString(),
32
+ baseUrl: options.url,
33
+ routes: [],
34
+ pagesSkipped: 0,
35
+ budgetExceeded: false,
36
+ },
37
+ repoInventory: null,
38
+ decisionLog: [
39
+ {
40
+ timestamp: new Date().toISOString(),
41
+ phase: 'observe',
42
+ decision: 'auth-required',
43
+ reason: detection.recommendation,
44
+ metadata: { detection },
45
+ },
46
+ ],
47
+ detectedAuth: detection,
48
+ };
49
+ }
50
+ }
11
51
  const observed = await observe(options.url, options.repoPath, options.config, artifacts);
12
52
  const analysis = await think(observed, options.config, artifacts);
13
53
  await act(analysis, options.config, artifacts);
package/dist/cli/index.js CHANGED
@@ -5,8 +5,17 @@ import { pathToFileURL } from 'node:url';
5
5
  import { z } from 'zod';
6
6
  import { HarnessConfigSchema } from '../schemas/config.schema.js';
7
7
  import { analyzeApp } from '../analyze.js';
8
+ import { detectAuth } from '../tools/auth-detector.js';
8
9
  const program = new Command();
9
10
  const AnalyzeUrlSchema = z.string().url();
11
+ const FormLoginCliSchema = z.object({
12
+ loginUrl: z.string().url(),
13
+ username: z.string().min(1),
14
+ password: z.string(),
15
+ usernameSelector: z.string().min(1),
16
+ passwordSelector: z.string().min(1),
17
+ submitSelector: z.string().min(1),
18
+ });
10
19
  async function loadConfigFile(relativePath) {
11
20
  const configPath = resolve(process.cwd(), relativePath);
12
21
  const configModule = await import(pathToFileURL(configPath).href);
@@ -25,10 +34,47 @@ function redactConfigForLog(config) {
25
34
  }
26
35
  return base;
27
36
  }
37
+ function mergeAuthFromCli(config, options) {
38
+ if (options.authStorageState && options.authFormLogin) {
39
+ throw new Error('Use either --auth-storage-state or --auth-form-login, not both.');
40
+ }
41
+ if (options.authStorageState) {
42
+ return {
43
+ ...config,
44
+ auth: { type: 'storage-state', path: options.authStorageState },
45
+ };
46
+ }
47
+ if (options.authFormLogin) {
48
+ const parsed = FormLoginCliSchema.parse({
49
+ loginUrl: options.loginUrl,
50
+ username: options.username,
51
+ password: options.password,
52
+ usernameSelector: options.usernameSelector,
53
+ passwordSelector: options.passwordSelector,
54
+ submitSelector: options.submitSelector,
55
+ });
56
+ return {
57
+ ...config,
58
+ auth: {
59
+ type: 'form-login',
60
+ loginUrl: parsed.loginUrl,
61
+ credentials: { username: parsed.username, password: parsed.password },
62
+ selectors: {
63
+ username: parsed.usernameSelector,
64
+ password: parsed.passwordSelector,
65
+ submit: parsed.submitSelector,
66
+ },
67
+ successIndicator: {},
68
+ },
69
+ };
70
+ }
71
+ return config;
72
+ }
28
73
  async function runAnalyze(options) {
29
74
  const validatedUrl = AnalyzeUrlSchema.parse(options.url);
30
75
  const mode = options.repo ? 'url-repo' : 'url-only';
31
- const config = await loadConfigFile(options.configFile ?? 'qulib.config.ts');
76
+ const baseConfig = await loadConfigFile(options.configFile ?? 'qulib.config.ts');
77
+ const config = mergeAuthFromCli(baseConfig, options);
32
78
  const ephemeral = options.ephemeral ?? false;
33
79
  const writeArtifacts = !ephemeral;
34
80
  if (ephemeral) {
@@ -50,6 +96,7 @@ async function runAnalyze(options) {
50
96
  discoveredRoutes: result.routeInventory,
51
97
  repoInventory: result.repoInventory,
52
98
  decisionLog: result.decisionLog,
99
+ ...(result.detectedAuth !== undefined && { detectedAuth: result.detectedAuth }),
53
100
  }, null, 2));
54
101
  }
55
102
  }
@@ -87,16 +134,86 @@ program
87
134
  .option('--config <file>', 'Path to config file (relative to cwd)', 'qulib.config.ts')
88
135
  .option('--adapter <type>', 'Override default test adapter (playwright, cypress-e2e, cypress-component, api)', 'playwright')
89
136
  .option('--ephemeral', 'Do not write to disk — return full report as JSON on stdout (use for MCP/CI)', false)
137
+ .option('--auth-storage-state <path>', 'Path to a storage state JSON file (use after `qulib auth init`)')
138
+ .option('--auth-form-login', 'Use form-login; requires --login-url, credentials, and selectors', false)
139
+ .option('--login-url <url>', 'Form login page URL (required with --auth-form-login)')
140
+ .option('--username <user>', 'Form login username')
141
+ .option('--password <secret>', 'Form login password')
142
+ .option('--username-selector <sel>', 'Selector for username field')
143
+ .option('--password-selector <sel>', 'Selector for password field')
144
+ .option('--submit-selector <sel>', 'Selector for submit control')
90
145
  .action(async (options) => {
146
+ const authFormLogin = Boolean(options.authFormLogin);
147
+ const loginUrl = options.loginUrl;
148
+ if (!authFormLogin && loginUrl !== undefined) {
149
+ throw new Error('--login-url is only valid with --auth-form-login');
150
+ }
151
+ if (authFormLogin && loginUrl === undefined) {
152
+ throw new Error('--auth-form-login requires --login-url');
153
+ }
91
154
  await runAnalyze({
92
155
  url: options.url,
93
156
  repo: options.repo,
94
157
  configFile: options.config,
95
158
  ephemeral: options.ephemeral,
159
+ authStorageState: options.authStorageState,
160
+ authFormLogin,
161
+ loginUrl,
162
+ username: options.username,
163
+ password: options.password,
164
+ usernameSelector: options.usernameSelector,
165
+ passwordSelector: options.passwordSelector,
166
+ submitSelector: options.submitSelector,
167
+ });
168
+ });
169
+ program
170
+ .command('detect-auth')
171
+ .description('Detect the authentication pattern used by a deployed web app')
172
+ .requiredOption('--url <url>', 'URL of the app or login page')
173
+ .option('--timeout <ms>', 'Page load timeout in ms', '15000')
174
+ .action(async (options) => {
175
+ const result = await detectAuth(options.url, parseInt(options.timeout, 10));
176
+ console.log(JSON.stringify(result, null, 2));
177
+ });
178
+ const authCmd = program.command('auth').description('Authentication helpers for scans');
179
+ authCmd
180
+ .command('init')
181
+ .description('Open a browser, let the user log in manually, save the storage state to a file for reuse')
182
+ .requiredOption('--base-url <url>', 'The base URL of the app to log into')
183
+ .option('--out <path>', 'Output file path for the storage state JSON', './qulib-storage-state.json')
184
+ .option('--timeout <ms>', 'Maximum time to wait for the user to finish logging in (default 5 min)', '300000')
185
+ .action(async (options) => {
186
+ const { chromium } = await import('@playwright/test');
187
+ const browser = await chromium.launch({ headless: false });
188
+ const context = await browser.newContext();
189
+ const page = await context.newPage();
190
+ const timeoutMs = parseInt(options.timeout, 10);
191
+ console.log(`\n[qulib] Opening ${options.baseUrl}`);
192
+ console.log('[qulib] Log in normally in the browser window that just opened.');
193
+ console.log('[qulib] After you reach a logged-in state, return to this terminal and press ENTER.');
194
+ console.log(`[qulib] You have ${timeoutMs / 1000}s before timeout.\n`);
195
+ await page.goto(options.baseUrl);
196
+ await new Promise((resolve, reject) => {
197
+ const timer = setTimeout(() => reject(new Error('Timed out waiting for user')), timeoutMs);
198
+ process.stdin.once('data', () => {
199
+ clearTimeout(timer);
200
+ resolve();
201
+ });
202
+ process.stdin.resume();
96
203
  });
204
+ const fs = await import('node:fs/promises');
205
+ const pathMod = await import('node:path');
206
+ const outPath = pathMod.resolve(options.out);
207
+ await context.storageState({ path: outPath });
208
+ console.log(`\n[qulib] Saved storage state to ${outPath}`);
209
+ console.log('[qulib] To use it, pass to qulib like:');
210
+ console.log(` qulib analyze --url ${options.baseUrl} --auth-storage-state ${outPath}`);
211
+ console.log(`[qulib] Or in MCP, pass auth: { type: 'storage-state', path: '${outPath}' }`);
212
+ await browser.close();
213
+ process.exit(0);
97
214
  });
98
215
  program.parseAsync().catch((error) => {
99
216
  const message = error instanceof Error ? error.message : String(error);
100
- console.error('[qulib] Analyze failed:', message);
217
+ console.error('[qulib] Failed:', message);
101
218
  process.exit(1);
102
219
  });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { analyzeApp } from './analyze.js';
2
+ export { detectAuth } from './tools/auth-detector.js';
2
3
  export type { AnalyzeOptions, AnalyzeResult } from './analyze.js';
3
- export type { HarnessConfig, AuthConfig, RouteInventory, GapAnalysis, RepoAnalysis, } from './schemas/index.js';
4
+ export type { HarnessConfig, AuthConfig, RouteInventory, GapAnalysis, RepoAnalysis, DetectedAuth, } from './schemas/index.js';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,GACb,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,YAAY,GACb,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { analyzeApp } from './analyze.js';
2
+ export { detectAuth } from './tools/auth-detector.js';
@@ -323,5 +323,66 @@ export declare const HarnessConfigSchema: z.ZodObject<{
323
323
  } | undefined;
324
324
  }>;
325
325
  export type HarnessConfig = z.infer<typeof HarnessConfigSchema>;
326
+ export declare const DetectedAuthSchema: z.ZodObject<{
327
+ hasAuth: z.ZodBoolean;
328
+ type: z.ZodEnum<["none", "form-login", "oauth", "magic-link", "unknown"]>;
329
+ provider: z.ZodNullable<z.ZodString>;
330
+ loginUrl: z.ZodNullable<z.ZodString>;
331
+ observedSelectors: z.ZodNullable<z.ZodObject<{
332
+ usernameSelector: z.ZodNullable<z.ZodString>;
333
+ passwordSelector: z.ZodNullable<z.ZodString>;
334
+ submitSelector: z.ZodNullable<z.ZodString>;
335
+ }, "strip", z.ZodTypeAny, {
336
+ usernameSelector: string | null;
337
+ passwordSelector: string | null;
338
+ submitSelector: string | null;
339
+ }, {
340
+ usernameSelector: string | null;
341
+ passwordSelector: string | null;
342
+ submitSelector: string | null;
343
+ }>>;
344
+ oauthButtons: z.ZodArray<z.ZodObject<{
345
+ provider: z.ZodString;
346
+ text: z.ZodString;
347
+ }, "strip", z.ZodTypeAny, {
348
+ provider: string;
349
+ text: string;
350
+ }, {
351
+ provider: string;
352
+ text: string;
353
+ }>, "many">;
354
+ recommendation: z.ZodString;
355
+ }, "strip", z.ZodTypeAny, {
356
+ type: "unknown" | "form-login" | "none" | "oauth" | "magic-link";
357
+ loginUrl: string | null;
358
+ hasAuth: boolean;
359
+ provider: string | null;
360
+ observedSelectors: {
361
+ usernameSelector: string | null;
362
+ passwordSelector: string | null;
363
+ submitSelector: string | null;
364
+ } | null;
365
+ oauthButtons: {
366
+ provider: string;
367
+ text: string;
368
+ }[];
369
+ recommendation: string;
370
+ }, {
371
+ type: "unknown" | "form-login" | "none" | "oauth" | "magic-link";
372
+ loginUrl: string | null;
373
+ hasAuth: boolean;
374
+ provider: string | null;
375
+ observedSelectors: {
376
+ usernameSelector: string | null;
377
+ passwordSelector: string | null;
378
+ submitSelector: string | null;
379
+ } | null;
380
+ oauthButtons: {
381
+ provider: string;
382
+ text: string;
383
+ }[];
384
+ recommendation: string;
385
+ }>;
386
+ export type DetectedAuth = z.infer<typeof DetectedAuthSchema>;
326
387
  export {};
327
388
  //# sourceMappingURL=config.schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/config.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;AACpD,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,aAAa,GAAG,mBAAmB,GAAG,KAAK,GAAG,eAAe,CAAC;AAEvG,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBvB,CAAC;AAEH,QAAA,MAAM,sBAAsB;;;;;;;;;EAG1B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA8E,CAAC;AAE5G,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACtE,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC5E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAe9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
1
+ {"version":3,"file":"config.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/config.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;AACpD,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,aAAa,GAAG,mBAAmB,GAAG,KAAK,GAAG,eAAe,CAAC;AAEvG,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBvB,CAAC;AAEH,QAAA,MAAM,sBAAsB;;;;;;;;;EAG1B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA8E,CAAC;AAE5G,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACtE,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC5E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAe9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
@@ -37,3 +37,21 @@ export const HarnessConfigSchema = z.object({
37
37
  adapters: z.array(z.enum(['playwright', 'cypress-e2e', 'cypress-component', 'api', 'accessibility'])).default(['playwright']),
38
38
  auth: AuthConfigSchema.optional(),
39
39
  });
40
+ export const DetectedAuthSchema = z.object({
41
+ hasAuth: z.boolean(),
42
+ type: z.enum(['none', 'form-login', 'oauth', 'magic-link', 'unknown']),
43
+ provider: z.string().nullable(),
44
+ loginUrl: z.string().nullable(),
45
+ observedSelectors: z
46
+ .object({
47
+ usernameSelector: z.string().nullable(),
48
+ passwordSelector: z.string().nullable(),
49
+ submitSelector: z.string().nullable(),
50
+ })
51
+ .nullable(),
52
+ oauthButtons: z.array(z.object({
53
+ provider: z.string(),
54
+ text: z.string(),
55
+ })),
56
+ recommendation: z.string(),
57
+ });
@@ -147,11 +147,11 @@ export declare const GeneratedTestSchema: z.ZodObject<{
147
147
  }>;
148
148
  export declare const GapAnalysisSchema: z.ZodObject<{
149
149
  analyzedAt: z.ZodString;
150
- mode: z.ZodEnum<["url-only", "url-repo"]>;
150
+ mode: z.ZodEnum<["url-only", "url-repo", "auth-required"]>;
151
151
  releaseConfidence: z.ZodNumber;
152
152
  coveragePagesScanned: z.ZodNumber;
153
153
  coverageBudgetExceeded: z.ZodBoolean;
154
- coverageWarning: z.ZodOptional<z.ZodEnum<["budget-exceeded", "low-coverage", "navigation-failures"]>>;
154
+ coverageWarning: z.ZodOptional<z.ZodEnum<["budget-exceeded", "low-coverage", "navigation-failures", "auth-required"]>>;
155
155
  gaps: z.ZodArray<z.ZodObject<{
156
156
  id: z.ZodString;
157
157
  path: z.ZodString;
@@ -271,7 +271,7 @@ export declare const GapAnalysisSchema: z.ZodObject<{
271
271
  }>, "many">;
272
272
  }, "strip", z.ZodTypeAny, {
273
273
  analyzedAt: string;
274
- mode: "url-only" | "url-repo";
274
+ mode: "url-only" | "url-repo" | "auth-required";
275
275
  releaseConfidence: number;
276
276
  coveragePagesScanned: number;
277
277
  coverageBudgetExceeded: boolean;
@@ -310,10 +310,10 @@ export declare const GapAnalysisSchema: z.ZodObject<{
310
310
  source: "llm" | "template";
311
311
  outputPath: string;
312
312
  }[];
313
- coverageWarning?: "budget-exceeded" | "low-coverage" | "navigation-failures" | undefined;
313
+ coverageWarning?: "auth-required" | "budget-exceeded" | "low-coverage" | "navigation-failures" | undefined;
314
314
  }, {
315
315
  analyzedAt: string;
316
- mode: "url-only" | "url-repo";
316
+ mode: "url-only" | "url-repo" | "auth-required";
317
317
  releaseConfidence: number;
318
318
  coveragePagesScanned: number;
319
319
  coverageBudgetExceeded: boolean;
@@ -352,7 +352,7 @@ export declare const GapAnalysisSchema: z.ZodObject<{
352
352
  source: "llm" | "template";
353
353
  outputPath: string;
354
354
  }[];
355
- coverageWarning?: "budget-exceeded" | "low-coverage" | "navigation-failures" | undefined;
355
+ coverageWarning?: "auth-required" | "budget-exceeded" | "low-coverage" | "navigation-failures" | undefined;
356
356
  }>;
357
357
  export type GapAnalysis = z.infer<typeof GapAnalysisSchema>;
358
358
  export type Gap = z.infer<typeof GapSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"gap-analysis.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/gap-analysis.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;EAMpB,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;EAIxC,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;EAgBzB,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUhC,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;EAO9B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAC5C,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACpE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC"}
1
+ {"version":3,"file":"gap-analysis.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/gap-analysis.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;EAMpB,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;EAIxC,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;EAgBzB,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUhC,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;EAO9B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAY5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAC5C,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACpE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC"}
@@ -49,11 +49,13 @@ export const GeneratedTestSchema = z.object({
49
49
  });
50
50
  export const GapAnalysisSchema = z.object({
51
51
  analyzedAt: z.string().datetime(),
52
- mode: z.enum(['url-only', 'url-repo']),
52
+ mode: z.enum(['url-only', 'url-repo', 'auth-required']),
53
53
  releaseConfidence: z.number().min(0).max(100),
54
54
  coveragePagesScanned: z.number().int().min(0),
55
55
  coverageBudgetExceeded: z.boolean(),
56
- coverageWarning: z.enum(['budget-exceeded', 'low-coverage', 'navigation-failures']).optional(),
56
+ coverageWarning: z
57
+ .enum(['budget-exceeded', 'low-coverage', 'navigation-failures', 'auth-required'])
58
+ .optional(),
57
59
  gaps: z.array(GapSchema),
58
60
  scenarios: z.array(NeutralScenarioSchema),
59
61
  generatedTests: z.array(GeneratedTestSchema),
@@ -1,4 +1,4 @@
1
- export { HarnessConfigSchema, AuthConfigSchema, type ExplorerType, type AdapterType, type FormLoginAuthConfig, type StorageStateAuthConfig, type AuthConfig, type HarnessConfig, } from './config.schema.js';
1
+ export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, type ExplorerType, type AdapterType, type FormLoginAuthConfig, type StorageStateAuthConfig, type AuthConfig, type HarnessConfig, type DetectedAuth, } 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,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,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,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,4 +1,4 @@
1
- export { HarnessConfigSchema, AuthConfigSchema, } from './config.schema.js';
1
+ export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, } 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';
@@ -0,0 +1,3 @@
1
+ import type { DetectedAuth } from '../schemas/config.schema.js';
2
+ export declare function detectAuth(url: string, timeoutMs?: number): Promise<DetectedAuth>;
3
+ //# sourceMappingURL=auth-detector.d.ts.map
@@ -0,0 +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;AA4DhE,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,CAkGtF"}
@@ -0,0 +1,144 @@
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
+ }
10
+ const OAUTH_PROVIDERS = [
11
+ { provider: 'github', patterns: [/github/i, /sign in with github/i] },
12
+ {
13
+ provider: 'google',
14
+ patterns: [/google/i, /sign in with google/i, /accounts\.google\.com/i],
15
+ },
16
+ {
17
+ provider: 'microsoft',
18
+ patterns: [/microsoft/i, /sign in with microsoft/i, /login\.microsoftonline\.com/i],
19
+ },
20
+ { provider: 'apple', patterns: [/apple/i, /sign in with apple/i] },
21
+ { provider: 'auth0', patterns: [/auth0/i] },
22
+ { provider: 'okta', patterns: [/okta/i] },
23
+ ];
24
+ function textLooksLikeOAuthIdpButton(text) {
25
+ const t = text.trim();
26
+ if (t.length === 0 || t.length > 120) {
27
+ return false;
28
+ }
29
+ return (/\b(sign in with|log in with|continue with|sign up with)\b/i.test(t) ||
30
+ /^(github|google|microsoft|apple)$/i.test(t));
31
+ }
32
+ const MAGIC_LINK_PATTERNS = [
33
+ /email me a (sign[- ]?in )?link/i,
34
+ /sign in with email/i,
35
+ /passwordless/i,
36
+ /we'll send you a link/i,
37
+ ];
38
+ async function firstTextInputNameForLogin(page) {
39
+ const emailName = await page.locator('input[type="email"]').first().getAttribute('name').catch(() => null);
40
+ if (emailName) {
41
+ return emailName;
42
+ }
43
+ const textInputs = page.locator('input[type="text"]');
44
+ const count = await textInputs.count();
45
+ for (let i = 0; i < count; i++) {
46
+ const name = await textInputs.nth(i).getAttribute('name');
47
+ if (name && /user|email|login/i.test(name)) {
48
+ return name;
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+ export async function detectAuth(url, timeoutMs = 15000) {
54
+ const browser = await launchBrowser();
55
+ try {
56
+ const context = await browser.newContext();
57
+ const page = await context.newPage();
58
+ await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
59
+ await waitNetworkIdleBestEffort(page);
60
+ let loginUrl = url;
61
+ const looksLikeLoginPage = /login|sign[- ]?in|auth/i.test(page.url()) ||
62
+ (await page.locator('input[type="password"]').count()) > 0;
63
+ if (!looksLikeLoginPage) {
64
+ const loginLink = page.locator('a').filter({ hasText: /^(log ?in|sign ?in|sign in)$/i }).first();
65
+ if ((await loginLink.count()) > 0) {
66
+ const href = await loginLink.getAttribute('href');
67
+ if (href) {
68
+ loginUrl = new URL(href, url).toString();
69
+ await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
70
+ await waitNetworkIdleBestEffort(page);
71
+ }
72
+ }
73
+ }
74
+ const passwordInputs = page.locator('input[type="password"]');
75
+ const passwordCount = await passwordInputs.count();
76
+ const hasFormLogin = passwordCount > 0;
77
+ const oauthButtons = [];
78
+ const buttonTexts = await page.locator('button, a').allInnerTexts();
79
+ for (const text of buttonTexts) {
80
+ const trimmed = text.trim();
81
+ if (!textLooksLikeOAuthIdpButton(trimmed)) {
82
+ continue;
83
+ }
84
+ for (const { provider, patterns } of OAUTH_PROVIDERS) {
85
+ if (patterns.some((p) => p.test(trimmed))) {
86
+ if (!oauthButtons.find((b) => b.provider === provider)) {
87
+ oauthButtons.push({ provider, text: trimmed.slice(0, 100) });
88
+ }
89
+ }
90
+ }
91
+ }
92
+ const pageText = await page.locator('body').innerText().catch(() => '');
93
+ const hasMagicLink = MAGIC_LINK_PATTERNS.some((p) => p.test(pageText));
94
+ let type = 'none';
95
+ let provider = null;
96
+ let observedSelectors = null;
97
+ let recommendation = '';
98
+ if (oauthButtons.length > 0) {
99
+ type = 'oauth';
100
+ provider = oauthButtons[0].provider;
101
+ recommendation = `OAuth detected (${oauthButtons.map((b) => b.provider).join(', ')}). OAuth cannot be automated. Run "qulib auth init --base-url ${url}" to log in manually once and save a reusable storage state file.`;
102
+ }
103
+ else if (hasFormLogin) {
104
+ type = 'form-login';
105
+ const usernameName = await firstTextInputNameForLogin(page);
106
+ const passwordName = await passwordInputs.first().getAttribute('name').catch(() => null);
107
+ const submitName = await page
108
+ .locator('button[type="submit"], input[type="submit"]')
109
+ .first()
110
+ .getAttribute('name')
111
+ .catch(() => null);
112
+ observedSelectors = {
113
+ usernameSelector: usernameName ? `input[name="${usernameName}"]` : null,
114
+ passwordSelector: passwordName ? `input[name="${passwordName}"]` : null,
115
+ submitSelector: submitName ? `button[name="${submitName}"]` : 'button[type="submit"]',
116
+ };
117
+ recommendation = `Form login detected. Configure auth with type="form-login", credentials, and the selectors above. Test selectors in your browser dev tools to confirm.`;
118
+ }
119
+ else if (hasMagicLink) {
120
+ type = 'magic-link';
121
+ recommendation = `Magic link / passwordless auth detected. Qulib cannot complete email-link flows. Run "qulib auth init --base-url ${url}" to log in manually once and save a storage state file.`;
122
+ }
123
+ else if (looksLikeLoginPage) {
124
+ type = 'unknown';
125
+ recommendation = `Authentication required but the pattern is unrecognized. Use "qulib auth init --base-url ${url}" to capture a storage state by logging in manually.`;
126
+ }
127
+ else {
128
+ type = 'none';
129
+ recommendation = `No authentication required for the entry URL. Qulib can scan anonymously.`;
130
+ }
131
+ return {
132
+ hasAuth: type !== 'none',
133
+ type,
134
+ provider,
135
+ loginUrl: type === 'none' ? null : loginUrl,
136
+ observedSelectors,
137
+ oauthButtons,
138
+ recommendation,
139
+ };
140
+ }
141
+ finally {
142
+ await browser.close();
143
+ }
144
+ }
@@ -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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"playwright-explorer.d.ts","sourceRoot":"","sources":["../../src/tools/playwright-explorer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAwB,KAAK,cAAc,EAAc,MAAM,sCAAsC,CAAC;AAC7G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAgBjE,qBAAa,kBAAmB,YAAW,WAAW;IAC9C,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;CAqJ/E"}
1
+ {"version":3,"file":"playwright-explorer.d.ts","sourceRoot":"","sources":["../../src/tools/playwright-explorer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAwB,KAAK,cAAc,EAAc,MAAM,sCAAsC,CAAC;AAC7G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAgBjE,qBAAa,kBAAmB,YAAW,WAAW;IAC9C,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;CAqJ/E"}
@@ -1,4 +1,4 @@
1
- import { chromium } from '@playwright/test';
1
+ import { launchBrowser } from './browser.js';
2
2
  import { AxeBuilder } from '@axe-core/playwright';
3
3
  import { createAuthenticatedContext } from './auth.js';
4
4
  import { RouteInventorySchema } from '../schemas/route-inventory.schema.js';
@@ -17,7 +17,7 @@ function isInternalHref(href, baseUrlStr) {
17
17
  }
18
18
  export class PlaywrightExplorer {
19
19
  async explore(baseUrl, config) {
20
- const browser = await chromium.launch({ headless: true });
20
+ const browser = await launchBrowser();
21
21
  let context;
22
22
  try {
23
23
  context = await createAuthenticatedContext(browser, config.auth, config.timeoutMs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qulib/core",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Qulib — analyze deployed web apps for honest quality gaps (CLI + programmatic API)",
5
5
  "license": "MIT",
6
6
  "author": "Tapesh Nagarwal",