@qulib/core 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -8
- package/dist/__tests__/cli-smoke-fixture.d.ts +2 -0
- package/dist/__tests__/cli-smoke-fixture.d.ts.map +1 -0
- package/dist/__tests__/cli-smoke-fixture.js +58 -0
- package/dist/__tests__/fixture-server.d.ts +6 -0
- package/dist/__tests__/fixture-server.d.ts.map +1 -0
- package/dist/__tests__/fixture-server.js +141 -0
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +84 -5
- package/dist/cli/auth-login-run.d.ts.map +1 -1
- package/dist/cli/auth-login-run.js +26 -2
- package/dist/cli/index.js +12 -6
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/llm/providers/anthropic.js +1 -1
- package/dist/phases/observe.js +2 -2
- package/dist/phases/think-finalize.d.ts.map +1 -1
- package/dist/phases/think-finalize.js +7 -1
- package/dist/phases/think.js +1 -1
- package/dist/schemas/automation-maturity.schema.d.ts +8 -0
- package/dist/schemas/automation-maturity.schema.d.ts.map +1 -1
- package/dist/schemas/automation-maturity.schema.js +1 -0
- package/dist/schemas/repo-analysis.schema.d.ts +7 -0
- package/dist/schemas/repo-analysis.schema.d.ts.map +1 -1
- package/dist/telemetry/telemetry.interface.d.ts +1 -1
- package/dist/telemetry/telemetry.interface.d.ts.map +1 -1
- package/dist/tools/apply-auth.d.ts +4 -0
- package/dist/tools/apply-auth.d.ts.map +1 -0
- package/dist/tools/apply-auth.js +35 -0
- package/dist/tools/auth/apply.d.ts +4 -0
- package/dist/tools/auth/apply.d.ts.map +1 -0
- package/dist/tools/auth/apply.js +35 -0
- package/dist/tools/auth/block-gap.d.ts +9 -0
- package/dist/tools/auth/block-gap.d.ts.map +1 -0
- package/dist/tools/auth/block-gap.js +52 -0
- package/dist/tools/auth/custom-providers.d.ts +15 -0
- package/dist/tools/auth/custom-providers.d.ts.map +1 -0
- package/dist/tools/auth/custom-providers.js +62 -0
- package/dist/tools/auth/detect.d.ts +23 -0
- package/dist/tools/auth/detect.d.ts.map +1 -0
- package/dist/tools/auth/detect.js +526 -0
- package/dist/tools/auth/detector.d.ts +23 -0
- package/dist/tools/auth/detector.d.ts.map +1 -0
- package/dist/tools/auth/detector.js +526 -0
- package/dist/tools/auth/explore.d.ts +4 -0
- package/dist/tools/auth/explore.d.ts.map +1 -0
- package/dist/tools/auth/explore.js +346 -0
- package/dist/tools/auth/explorer.d.ts +4 -0
- package/dist/tools/auth/explorer.d.ts.map +1 -0
- package/dist/tools/auth/explorer.js +346 -0
- package/dist/tools/auth/gaps.d.ts +9 -0
- package/dist/tools/auth/gaps.d.ts.map +1 -0
- package/dist/tools/auth/gaps.js +52 -0
- package/dist/tools/auth/oauth-providers.d.ts +7 -0
- package/dist/tools/auth/oauth-providers.d.ts.map +1 -0
- package/dist/tools/auth/oauth-providers.js +21 -0
- package/dist/tools/auth/providers.d.ts +7 -0
- package/dist/tools/auth/providers.d.ts.map +1 -0
- package/dist/tools/auth/providers.js +21 -0
- package/dist/tools/auth/surface-analyzer.d.ts +4 -0
- package/dist/tools/auth/surface-analyzer.d.ts.map +1 -0
- package/dist/tools/auth/surface-analyzer.js +170 -0
- package/dist/tools/auth/surface.d.ts +4 -0
- package/dist/tools/auth/surface.d.ts.map +1 -0
- package/dist/tools/auth/surface.js +170 -0
- package/dist/tools/auth/user-providers.d.ts +15 -0
- package/dist/tools/auth/user-providers.d.ts.map +1 -0
- package/dist/tools/auth/user-providers.js +62 -0
- package/dist/tools/auth-block-gap.d.ts +6 -0
- package/dist/tools/auth-block-gap.d.ts.map +1 -1
- package/dist/tools/auth-block-gap.js +42 -9
- package/dist/tools/auth-detector.d.ts +9 -8
- package/dist/tools/auth-detector.d.ts.map +1 -1
- package/dist/tools/auth-detector.js +106 -8
- package/dist/tools/explorers/browser.d.ts +3 -0
- package/dist/tools/explorers/browser.d.ts.map +1 -0
- package/dist/tools/explorers/browser.js +13 -0
- package/dist/tools/explorers/cypress-explorer.d.ts +8 -0
- package/dist/tools/explorers/cypress-explorer.d.ts.map +1 -0
- package/dist/tools/explorers/cypress-explorer.js +5 -0
- package/dist/tools/explorers/cypress.d.ts +8 -0
- package/dist/tools/explorers/cypress.d.ts.map +1 -0
- package/dist/tools/explorers/cypress.js +5 -0
- package/dist/tools/explorers/explorer.interface.d.ts +7 -0
- package/dist/tools/explorers/explorer.interface.d.ts.map +1 -0
- package/dist/tools/explorers/explorer.interface.js +1 -0
- package/dist/tools/explorers/factory.d.ts +4 -0
- package/dist/tools/explorers/factory.d.ts.map +1 -0
- package/dist/tools/explorers/factory.js +12 -0
- package/dist/tools/explorers/playwright-explorer.d.ts +8 -0
- package/dist/tools/explorers/playwright-explorer.d.ts.map +1 -0
- package/dist/tools/explorers/playwright-explorer.js +172 -0
- package/dist/tools/explorers/playwright.d.ts +8 -0
- package/dist/tools/explorers/playwright.d.ts.map +1 -0
- package/dist/tools/explorers/playwright.js +172 -0
- package/dist/tools/explorers/types.d.ts +7 -0
- package/dist/tools/explorers/types.d.ts.map +1 -0
- package/dist/tools/explorers/types.js +1 -0
- package/dist/tools/playwright-explorer.js +1 -1
- package/dist/tools/repo/detect-framework.d.ts +15 -0
- package/dist/tools/repo/detect-framework.d.ts.map +1 -0
- package/dist/tools/repo/detect-framework.js +153 -0
- package/dist/tools/repo/framework-detector.d.ts +15 -0
- package/dist/tools/repo/framework-detector.d.ts.map +1 -0
- package/dist/tools/repo/framework-detector.js +153 -0
- package/dist/tools/repo/scan.d.ts +19 -0
- package/dist/tools/repo/scan.d.ts.map +1 -0
- package/dist/tools/repo/scan.js +181 -0
- package/dist/tools/repo/scanner.d.ts +19 -0
- package/dist/tools/repo/scanner.d.ts.map +1 -0
- package/dist/tools/repo/scanner.js +181 -0
- package/dist/tools/scoring/automation-maturity.d.ts +4 -0
- package/dist/tools/scoring/automation-maturity.d.ts.map +1 -0
- package/dist/tools/scoring/automation-maturity.js +231 -0
- package/dist/tools/scoring/gap-engine.d.ts +8 -0
- package/dist/tools/scoring/gap-engine.d.ts.map +1 -0
- package/dist/tools/scoring/gap-engine.js +138 -0
- package/dist/tools/scoring/gaps.d.ts +8 -0
- package/dist/tools/scoring/gaps.d.ts.map +1 -0
- package/dist/tools/scoring/gaps.js +138 -0
- package/dist/tools/scoring/public-surface.d.ts +5 -0
- package/dist/tools/scoring/public-surface.d.ts.map +1 -0
- package/dist/tools/scoring/public-surface.js +13 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -77,6 +77,24 @@ qulib analyze --url https://app.example.com --auth-storage-state ./qulib-storage
|
|
|
77
77
|
|
|
78
78
|
The storage state is just a JSON file of cookies and localStorage — keep it private, treat it like a credential.
|
|
79
79
|
|
|
80
|
+
#### Storage state is validated before crawl
|
|
81
|
+
|
|
82
|
+
Qulib now validates the provided storage state before doing any work. If the file is missing, unreadable, empty, on the wrong origin, or carries a session that is already expired, Qulib stops with an honest `blocked` result (no fake `releaseConfidence`) and a structured gap explaining how to recover. The validator reports one of these stable reason codes:
|
|
83
|
+
|
|
84
|
+
| Reason code | Meaning |
|
|
85
|
+
| ------------------------- | ----------------------------------------------------------------------- |
|
|
86
|
+
| `missing-file` | Path passed to `--auth-storage-state` does not exist. |
|
|
87
|
+
| `unreadable-file` | File exists but the process can't read it (permissions). |
|
|
88
|
+
| `invalid-json` | File is present and readable but not valid JSON. |
|
|
89
|
+
| `no-auth-cookies` | File parses, but has zero cookies and zero localStorage entries. |
|
|
90
|
+
| `wrong-origin` | Session redirects to a different origin (host/port/scheme mismatch). |
|
|
91
|
+
| `expired-or-unauthorized` | Loaded session shows the login form again, or the app returns 401/403. |
|
|
92
|
+
| `unknown` | Validation could not be completed for an unexpected reason. |
|
|
93
|
+
|
|
94
|
+
Origin matching is strict — `https://app.example` and `https://www.app.example` are different origins, as are `http://localhost:3000` and `http://localhost:4000`. Re-run `qulib auth login` against the same origin you plan to `analyze`.
|
|
95
|
+
|
|
96
|
+
Relatedly, `qulib auth login` will now refuse to save a storage state if the browser ends the flow on a different origin than `--base-url` (a federated/SSO redirect that never returned to the app). This prevents Qulib from quietly persisting an IdP-domain session that would later produce false-confidence scans.
|
|
97
|
+
|
|
80
98
|
### Multi-path auth exploration (`explore-auth`)
|
|
81
99
|
|
|
82
100
|
For unfamiliar apps (especially enterprise SSO with several buttons), run **`qulib explore-auth --url <url>`** before `analyze`. The JSON lists every detected path (built-in OAuth names like Google/Clever, **heuristic** unknown buttons such as tenant-specific SSO labels, password forms, and magic-link copy) plus **`suggestedAgentBehavior`** for the agent.
|
|
@@ -194,16 +212,24 @@ TypeScript (strict, NodeNext), Commander, Zod, Playwright, @axe-core/playwright,
|
|
|
194
212
|
```text
|
|
195
213
|
src/
|
|
196
214
|
adapters/ # test rendering adapters
|
|
197
|
-
analyze.ts
|
|
198
|
-
cli/
|
|
199
|
-
harness/
|
|
200
|
-
llm/
|
|
201
|
-
phases/
|
|
202
|
-
reporters/
|
|
203
|
-
schemas/
|
|
204
|
-
|
|
215
|
+
analyze.ts # programmatic API (also used by @qulib/mcp)
|
|
216
|
+
cli/ # CLI entry
|
|
217
|
+
harness/ # state + decision logging
|
|
218
|
+
llm/ # LLM contracts
|
|
219
|
+
phases/ # observe / think / act
|
|
220
|
+
reporters/ # JSON + Markdown reports
|
|
221
|
+
schemas/ # Zod schemas
|
|
222
|
+
telemetry/ # event sink + URL redaction
|
|
223
|
+
tools/
|
|
224
|
+
auth/ # detection, exploration, validation, providers, gap builders
|
|
225
|
+
explorers/ # browser launch, Playwright/Cypress crawlers, factory
|
|
226
|
+
repo/ # repo scanner, framework detection
|
|
227
|
+
scoring/ # gap engine, automation maturity, public surface
|
|
228
|
+
__tests__/ # integration and wiring tests live in __tests__/ in each folder
|
|
205
229
|
```
|
|
206
230
|
|
|
231
|
+
A contributor map of which folder to touch for each kind of change lives at [`docs/source-map.md`](../../docs/source-map.md).
|
|
232
|
+
|
|
207
233
|
Repo rules: see [`CLAUDE.md`](../../CLAUDE.md).
|
|
208
234
|
|
|
209
235
|
## Configuration
|
|
@@ -251,6 +277,107 @@ npm run analyze -- --url https://example.com --ephemeral > report.bundle.json
|
|
|
251
277
|
npm run clean
|
|
252
278
|
```
|
|
253
279
|
|
|
280
|
+
## Minimum config
|
|
281
|
+
|
|
282
|
+
Smallest legal `qulib.config.ts`:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import type { HarnessConfig } from './src/schemas/config.schema.js';
|
|
286
|
+
|
|
287
|
+
const config: HarnessConfig = {
|
|
288
|
+
maxPagesToScan: 20,
|
|
289
|
+
maxDepth: 3,
|
|
290
|
+
timeoutMs: 30000,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export default config;
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
All other fields inherit from schema defaults or CLI/runtime defaults.
|
|
297
|
+
|
|
298
|
+
## Scan walkthroughs (copy-paste)
|
|
299
|
+
|
|
300
|
+
### 1) Public scan
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
npx @qulib/core analyze --url https://yourapp.com
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 2) Auth-blocked scan (honest blocked mode)
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
npx @qulib/core analyze --url https://yourapp.com/auth
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
When auth blocks access and no auth config is supplied, Qulib reports `status: "blocked"` (or `partial` if it could still crawl some public pages). This is intentional honesty, not a failure mode.
|
|
313
|
+
|
|
314
|
+
### 3) Authenticated scan with storage state
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# Capture once (manual OAuth/SSO-safe flow)
|
|
318
|
+
qulib auth init --base-url https://yourapp.com
|
|
319
|
+
|
|
320
|
+
# Reuse saved session
|
|
321
|
+
qulib analyze --url https://yourapp.com --auth-storage-state ./qulib-storage-state.json
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Sample report (fixture baseline)
|
|
325
|
+
|
|
326
|
+
From the local fixture baseline used in v0.5.0 PR 1/2:
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
{
|
|
330
|
+
"status": "complete",
|
|
331
|
+
"releaseConfidence": 68,
|
|
332
|
+
"gaps": [
|
|
333
|
+
"... 4 total gap items ..."
|
|
334
|
+
]
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Use these as conservative reference numbers:
|
|
339
|
+
- public fixture (`/`): `releaseConfidence: 68/100`, `gaps: 4`
|
|
340
|
+
- auth-wall fixture (`/auth`): `releaseConfidence: 24/100`, `gaps: 2`
|
|
341
|
+
- broken fixture (`/broken`): `releaseConfidence: 0/100`, `gaps: 6`
|
|
342
|
+
|
|
343
|
+
## MCP tools quick map
|
|
344
|
+
|
|
345
|
+
| Tool | When to use | Key input |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| `analyze_app` | Main QA scan for release confidence + gaps | `url`, optional `auth`, optional LLM knobs |
|
|
348
|
+
| `detect_auth` | Fast single-pass auth pattern guess | `url`, optional `timeoutMs` |
|
|
349
|
+
| `explore_auth` | Deeper auth-path discovery on unfamiliar apps | `url`, optional `timeoutMs` |
|
|
350
|
+
| `qulib_score_automation` | Score local repo automation maturity | absolute `repoPath`, optional `includeFullDimensions` |
|
|
351
|
+
|
|
352
|
+
## Output directories
|
|
353
|
+
|
|
354
|
+
Qulib writes runtime artifacts to:
|
|
355
|
+
|
|
356
|
+
- `.scan-state/` — intermediate state (discovered routes, gap analysis snapshots, decision log)
|
|
357
|
+
- `output/` — final `report.json` and `report.md`
|
|
358
|
+
|
|
359
|
+
Both are gitignored and safe to delete; Qulib recreates them on the next non-ephemeral run.
|
|
360
|
+
|
|
361
|
+
## ANTHROPIC_API_KEY (LLM scenarios)
|
|
362
|
+
|
|
363
|
+
For MCP-hosted usage, set `ANTHROPIC_API_KEY` in your host's `env` block:
|
|
364
|
+
|
|
365
|
+
```json
|
|
366
|
+
{
|
|
367
|
+
"mcpServers": {
|
|
368
|
+
"qulib": {
|
|
369
|
+
"command": "npx",
|
|
370
|
+
"args": ["@qulib/mcp"],
|
|
371
|
+
"env": {
|
|
372
|
+
"ANTHROPIC_API_KEY": "sk-ant-..."
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Without this key, Qulib still runs deterministic checks (crawl, a11y, links, console, scoring) and falls back to template scenarios instead of LLM-generated ones.
|
|
380
|
+
|
|
254
381
|
## Playwright browsers
|
|
255
382
|
|
|
256
383
|
```bash
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-smoke-fixture.d.ts","sourceRoot":"","sources":["../../src/__tests__/cli-smoke-fixture.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline CLI smoke: spawn `node bin/qulib.js analyze --url <fixture>` against
|
|
3
|
+
* the local fixture server and assert the CLI exited 0. Runnable script (not a
|
|
4
|
+
* node:test file) — invoked in CI by `node --import tsx/esm src/__tests__/cli-smoke-fixture.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Removes the live `https://example.com` dependency from CI's smoke-test-cli job.
|
|
7
|
+
*
|
|
8
|
+
* The fixture server runs in this process; the CLI is spawned as a child. We use
|
|
9
|
+
* async `spawn` (not `spawnSync`) so the parent event loop stays free to serve
|
|
10
|
+
* the child's HTTP requests against the fixture.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { dirname, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { startFixtureServer } from './fixture-server.js';
|
|
16
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const cliPath = resolve(__dir, '../../bin/qulib.js');
|
|
18
|
+
function runCli(url) {
|
|
19
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
20
|
+
const child = spawn('node', [cliPath, 'analyze', '--url', url, '--ephemeral'], {
|
|
21
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
22
|
+
});
|
|
23
|
+
const stdoutChunks = [];
|
|
24
|
+
const stderrChunks = [];
|
|
25
|
+
child.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
26
|
+
child.stderr.on('data', (chunk) => stderrChunks.push(chunk));
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
child.kill('SIGKILL');
|
|
29
|
+
rejectPromise(new Error('CLI smoke timed out after 120s'));
|
|
30
|
+
}, 120_000);
|
|
31
|
+
child.on('error', (err) => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
rejectPromise(err);
|
|
34
|
+
});
|
|
35
|
+
child.on('close', (code) => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
resolvePromise({
|
|
38
|
+
exitCode: code ?? -1,
|
|
39
|
+
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
40
|
+
stderr: Buffer.concat(stderrChunks).toString('utf8'),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function assertCliPassed(result) {
|
|
46
|
+
if (result.exitCode !== 0) {
|
|
47
|
+
throw new Error(`CLI exited with code ${result.exitCode}\n--- stdout ---\n${result.stdout}\n--- stderr ---\n${result.stderr}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const handle = await startFixtureServer();
|
|
51
|
+
try {
|
|
52
|
+
const result = await runCli(`${handle.baseUrl}/`);
|
|
53
|
+
assertCliPassed(result);
|
|
54
|
+
console.log('[cli-smoke] ✔ CLI exited 0 against fixture public surface');
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await handle.close();
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture-server.d.ts","sourceRoot":"","sources":["../../src/__tests__/fixture-server.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAyGD,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,CAAC,CA0CvE"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Node.js fixture server for offline Qulib integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Serves the static HTML files in `packages/core/fixtures/` over loopback so
|
|
5
|
+
* the test suite never depends on a live website. Used by
|
|
6
|
+
* `analyze.fixtures.test.ts` and any future offline integration coverage.
|
|
7
|
+
*
|
|
8
|
+
* Never imported by product code. Helpers are private to this module; only
|
|
9
|
+
* `startFixtureServer` and `FixtureServerHandle` are exported.
|
|
10
|
+
*/
|
|
11
|
+
import { createServer } from 'node:http';
|
|
12
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
13
|
+
import { dirname, join, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
const HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
|
|
16
|
+
const TEXT_CONTENT_TYPE = 'text/plain; charset=utf-8';
|
|
17
|
+
function resolveFixturesDir() {
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
return resolve(here, '../../fixtures');
|
|
20
|
+
}
|
|
21
|
+
function routeToFile(pathname) {
|
|
22
|
+
if (pathname.includes('..'))
|
|
23
|
+
return null;
|
|
24
|
+
const fixturesDir = resolveFixturesDir();
|
|
25
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
26
|
+
return join(fixturesDir, 'public/index.html');
|
|
27
|
+
}
|
|
28
|
+
if (pathname === '/about') {
|
|
29
|
+
return join(fixturesDir, 'public/about.html');
|
|
30
|
+
}
|
|
31
|
+
if (pathname === '/features') {
|
|
32
|
+
return join(fixturesDir, 'public/features.html');
|
|
33
|
+
}
|
|
34
|
+
if (pathname === '/docs') {
|
|
35
|
+
return join(fixturesDir, 'public/index.html');
|
|
36
|
+
}
|
|
37
|
+
if (pathname === '/auth') {
|
|
38
|
+
return join(fixturesDir, 'auth-wall/index.html');
|
|
39
|
+
}
|
|
40
|
+
if (pathname === '/authenticated' || pathname.startsWith('/authenticated/')) {
|
|
41
|
+
return join(fixturesDir, 'authenticated/index.html');
|
|
42
|
+
}
|
|
43
|
+
if (pathname === '/broken') {
|
|
44
|
+
return join(fixturesDir, 'broken/index.html');
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
async function readFixture(filePath) {
|
|
49
|
+
return readFile(filePath);
|
|
50
|
+
}
|
|
51
|
+
function respond(res, status, body, contentType) {
|
|
52
|
+
const buf = typeof body === 'string' ? Buffer.from(body, 'utf8') : body;
|
|
53
|
+
res.writeHead(status, {
|
|
54
|
+
'Content-Type': contentType,
|
|
55
|
+
'Content-Length': buf.length,
|
|
56
|
+
'Cache-Control': 'no-store',
|
|
57
|
+
});
|
|
58
|
+
res.end(buf);
|
|
59
|
+
}
|
|
60
|
+
function respondNotFound(res) {
|
|
61
|
+
respond(res, 404, 'Not found', TEXT_CONTENT_TYPE);
|
|
62
|
+
}
|
|
63
|
+
function respondServerError(res, message) {
|
|
64
|
+
respond(res, 500, `Fixture server error: ${message}`, TEXT_CONTENT_TYPE);
|
|
65
|
+
}
|
|
66
|
+
function respondMethodNotAllowed(res) {
|
|
67
|
+
res.writeHead(405, {
|
|
68
|
+
Allow: 'GET',
|
|
69
|
+
'Content-Type': TEXT_CONTENT_TYPE,
|
|
70
|
+
'Cache-Control': 'no-store',
|
|
71
|
+
});
|
|
72
|
+
res.end('Method Not Allowed');
|
|
73
|
+
}
|
|
74
|
+
async function handleRequest(req, res) {
|
|
75
|
+
try {
|
|
76
|
+
if (req.method !== 'GET') {
|
|
77
|
+
respondMethodNotAllowed(res);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const rawUrl = req.url ?? '/';
|
|
81
|
+
let pathname;
|
|
82
|
+
try {
|
|
83
|
+
pathname = new URL(rawUrl, 'http://127.0.0.1').pathname;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
respondNotFound(res);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const filePath = routeToFile(pathname);
|
|
90
|
+
if (filePath === null) {
|
|
91
|
+
respondNotFound(res);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const body = await readFixture(filePath);
|
|
95
|
+
respond(res, 200, body, HTML_CONTENT_TYPE);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
respondServerError(res, message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function startFixtureServer() {
|
|
103
|
+
const fixturesDir = resolveFixturesDir();
|
|
104
|
+
try {
|
|
105
|
+
const s = await stat(fixturesDir);
|
|
106
|
+
if (!s.isDirectory()) {
|
|
107
|
+
throw new Error(`fixtures path is not a directory: ${fixturesDir}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
112
|
+
throw new Error(`Fixture directory not found at ${fixturesDir}: ${detail}`);
|
|
113
|
+
}
|
|
114
|
+
const server = createServer((req, res) => {
|
|
115
|
+
void handleRequest(req, res);
|
|
116
|
+
});
|
|
117
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
118
|
+
server.once('error', rejectPromise);
|
|
119
|
+
server.listen(0, '127.0.0.1', () => {
|
|
120
|
+
server.off('error', rejectPromise);
|
|
121
|
+
resolvePromise();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
const address = server.address();
|
|
125
|
+
if (address === null || typeof address === 'string') {
|
|
126
|
+
server.close();
|
|
127
|
+
throw new Error('Fixture server did not return a usable address after listen');
|
|
128
|
+
}
|
|
129
|
+
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
130
|
+
return {
|
|
131
|
+
baseUrl,
|
|
132
|
+
close: () => new Promise((resolvePromise, rejectPromise) => {
|
|
133
|
+
server.close((err) => {
|
|
134
|
+
if (err)
|
|
135
|
+
rejectPromise(err);
|
|
136
|
+
else
|
|
137
|
+
resolvePromise();
|
|
138
|
+
});
|
|
139
|
+
}),
|
|
140
|
+
};
|
|
141
|
+
}
|
package/dist/analyze.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAChG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAU7F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAGxE,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/D,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;IAC5B,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,0GAA0G;IAC1G,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,sFAAsF;IACtF,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,WAAW,EAAE,WAAW,CAAC;IACzB,8GAA8G;IAC9G,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,0HAA0H;IAC1H,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;CACrC;AAcD,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAChG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAU7F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAGxE,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/D,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;IAC5B,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,0GAA0G;IAC1G,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,sFAAsF;IACtF,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,WAAW,EAAE,WAAW,CAAC;IACzB,8GAA8G;IAC9G,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,0HAA0H;IAC1H,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;CACrC;AAcD,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAqQhF"}
|
package/dist/analyze.js
CHANGED
|
@@ -4,11 +4,11 @@ import { PublicSurfaceSchema } from './schemas/public-surface.schema.js';
|
|
|
4
4
|
import { observe } from './phases/observe.js';
|
|
5
5
|
import { think } from './phases/think.js';
|
|
6
6
|
import { act } from './phases/act.js';
|
|
7
|
-
import { detectAuth } from './tools/auth
|
|
8
|
-
import { analyzeGaps, computeCoverageScore, computeQualityScoreFromGaps } from './tools/
|
|
9
|
-
import { analyzeAuthSurfaceGaps } from './tools/auth
|
|
10
|
-
import { buildPublicSurface } from './tools/public-surface.js';
|
|
11
|
-
import { buildAuthBlockGap } from './tools/auth
|
|
7
|
+
import { detectAuth, validateStorageState } from './tools/auth/detect.js';
|
|
8
|
+
import { analyzeGaps, computeCoverageScore, computeQualityScoreFromGaps } from './tools/scoring/gaps.js';
|
|
9
|
+
import { analyzeAuthSurfaceGaps } from './tools/auth/surface.js';
|
|
10
|
+
import { buildPublicSurface } from './tools/scoring/public-surface.js';
|
|
11
|
+
import { buildAuthBlockGap, buildStorageStateInvalidGap } from './tools/auth/gaps.js';
|
|
12
12
|
import { finalizeGapAnalysisFromDraft } from './phases/think-finalize.js';
|
|
13
13
|
import { emitTelemetry, redactUrlForTelemetry } from './telemetry/emit.js';
|
|
14
14
|
function logScanEnd(progress, result) {
|
|
@@ -40,6 +40,85 @@ export async function analyzeApp(options) {
|
|
|
40
40
|
hasAuth: Boolean(options.config.auth),
|
|
41
41
|
});
|
|
42
42
|
progress?.info(`Starting scan → ${options.url} maxPagesToScan=${options.config.maxPagesToScan}`);
|
|
43
|
+
if (options.config.auth?.type === 'storage-state') {
|
|
44
|
+
progress?.info('Validating provided storage state before crawl…');
|
|
45
|
+
const validation = await validateStorageState(options.url, options.config.auth.path, options.config.timeoutMs);
|
|
46
|
+
let targetOriginForTelemetry;
|
|
47
|
+
try {
|
|
48
|
+
targetOriginForTelemetry = new URL(options.url).origin;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
targetOriginForTelemetry = '[unparseable-target-url]';
|
|
52
|
+
}
|
|
53
|
+
emitTelemetry(options.telemetry, 'auth.storage-state.validated', sessionId, {
|
|
54
|
+
targetOrigin: targetOriginForTelemetry,
|
|
55
|
+
valid: validation.valid,
|
|
56
|
+
reasonCode: validation.reasonCode,
|
|
57
|
+
storageStateProvided: true,
|
|
58
|
+
});
|
|
59
|
+
if (!validation.valid) {
|
|
60
|
+
progress?.warn(`Storage state rejected (${validation.reasonCode}): ${validation.reason}. Skipping crawl.`);
|
|
61
|
+
decisionLog.push({
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
phase: 'observe',
|
|
64
|
+
decision: 'storage-state-invalid',
|
|
65
|
+
reason: `${validation.reasonCode}: ${validation.reason}`,
|
|
66
|
+
metadata: {
|
|
67
|
+
reasonCode: validation.reasonCode,
|
|
68
|
+
targetOrigin: targetOriginForTelemetry,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const invalidGap = buildStorageStateInvalidGap({
|
|
72
|
+
url: options.url,
|
|
73
|
+
reasonCode: validation.reasonCode === 'ok' ? 'unknown' : validation.reasonCode,
|
|
74
|
+
reason: validation.reason,
|
|
75
|
+
});
|
|
76
|
+
const draft = {
|
|
77
|
+
analyzedAt: new Date().toISOString(),
|
|
78
|
+
mode: 'auth-required',
|
|
79
|
+
releaseConfidence: 0,
|
|
80
|
+
coveragePagesScanned: 0,
|
|
81
|
+
coverageBudgetExceeded: false,
|
|
82
|
+
coverageWarning: 'auth-required',
|
|
83
|
+
gaps: [invalidGap],
|
|
84
|
+
};
|
|
85
|
+
const costContext = {
|
|
86
|
+
mode: 'auth-required',
|
|
87
|
+
coveragePagesScanned: 0,
|
|
88
|
+
releaseConfidence: 0,
|
|
89
|
+
gaps: [invalidGap],
|
|
90
|
+
};
|
|
91
|
+
const gapAnalysis = await finalizeGapAnalysisFromDraft(draft, options.config, artifacts, costContext);
|
|
92
|
+
const emptyAuthRoutes = RouteInventorySchema.parse({
|
|
93
|
+
scannedAt: new Date().toISOString(),
|
|
94
|
+
baseUrl: options.url,
|
|
95
|
+
routes: [],
|
|
96
|
+
pagesSkipped: 0,
|
|
97
|
+
budgetExceeded: false,
|
|
98
|
+
});
|
|
99
|
+
await act(gapAnalysis, options.config, artifacts);
|
|
100
|
+
const blockedResult = {
|
|
101
|
+
status: 'blocked',
|
|
102
|
+
coverageScore: null,
|
|
103
|
+
releaseConfidence: 0,
|
|
104
|
+
gaps: gapAnalysis.gaps,
|
|
105
|
+
gapAnalysis,
|
|
106
|
+
routeInventory: emptyAuthRoutes,
|
|
107
|
+
repoInventory: null,
|
|
108
|
+
decisionLog,
|
|
109
|
+
publicSurface: null,
|
|
110
|
+
};
|
|
111
|
+
logScanEnd(progress, blockedResult);
|
|
112
|
+
emitTelemetry(options.telemetry, 'scan.blocked', sessionId, {
|
|
113
|
+
status: blockedResult.status,
|
|
114
|
+
coverageScore: blockedResult.coverageScore,
|
|
115
|
+
releaseConfidence: blockedResult.releaseConfidence,
|
|
116
|
+
gapCount: blockedResult.gaps.length,
|
|
117
|
+
reasonCode: validation.reasonCode,
|
|
118
|
+
});
|
|
119
|
+
return blockedResult;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
43
122
|
let detectedAuth;
|
|
44
123
|
let authWall = false;
|
|
45
124
|
if (!options.config.auth && !options.skipAuthDetection) {
|
|
@@ -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;
|
|
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,4 +1,5 @@
|
|
|
1
|
-
import { BUILT_IN_OAUTH_PROVIDERS } from '../tools/
|
|
1
|
+
import { BUILT_IN_OAUTH_PROVIDERS } from '../tools/auth/providers.js';
|
|
2
|
+
import { waitForReturnToOrigin } from '../tools/auth/detect.js';
|
|
2
3
|
const builtInOAuthIds = new Set(BUILT_IN_OAUTH_PROVIDERS.map((p) => p.id));
|
|
3
4
|
function escapeRegExp(s) {
|
|
4
5
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -109,8 +110,31 @@ export async function runAutomatedAuthLogin(params) {
|
|
|
109
110
|
await sleep(250);
|
|
110
111
|
}
|
|
111
112
|
}
|
|
113
|
+
const originReturn = await waitForReturnToOrigin(page, params.baseUrlHint, params.timeoutMs);
|
|
114
|
+
if (!originReturn.returned) {
|
|
115
|
+
let targetOrigin = '<unknown>';
|
|
116
|
+
let finalOrigin = '<unknown>';
|
|
117
|
+
try {
|
|
118
|
+
targetOrigin = new URL(params.baseUrlHint).origin;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* targetOrigin stays <unknown> */
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
finalOrigin = new URL(originReturn.finalUrl).origin;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
/* finalOrigin stays <unknown> */
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Login flow did not return to the app origin (expected ${targetOrigin}, final ${finalOrigin}). ` +
|
|
130
|
+
`Refusing to save the storage state — it would belong to the wrong domain and produce ` +
|
|
131
|
+
`false-confidence scans. Retry the login (the federated provider may need a redirect tweak) ` +
|
|
132
|
+
`or capture the session manually with \`qulib auth init --base-url ${params.baseUrlHint}\`.`);
|
|
133
|
+
}
|
|
112
134
|
if (!confirmed) {
|
|
113
|
-
console.error('[qulib] Could not confirm login success
|
|
135
|
+
console.error('[qulib] Could not confirm login success heuristically, but the browser ended on the app origin. ' +
|
|
136
|
+
'Storage state will be saved; verify the session before relying on it (run `qulib analyze` ' +
|
|
137
|
+
'and check that releaseConfidence is not null).');
|
|
114
138
|
}
|
|
115
139
|
const fs = await import('node:fs/promises');
|
|
116
140
|
const pathMod = await import('node:path');
|
package/dist/cli/index.js
CHANGED
|
@@ -8,8 +8,8 @@ const requirePkg = createRequire(import.meta.url);
|
|
|
8
8
|
const pkg = requirePkg('../../package.json');
|
|
9
9
|
import { HarnessConfigSchema } from '../schemas/config.schema.js';
|
|
10
10
|
import { analyzeApp } from '../analyze.js';
|
|
11
|
-
import { detectAuth } from '../tools/auth
|
|
12
|
-
import { exploreAuth } from '../tools/auth
|
|
11
|
+
import { detectAuth } from '../tools/auth/detect.js';
|
|
12
|
+
import { exploreAuth } from '../tools/auth/explore.js';
|
|
13
13
|
import { assertExactlyOneCredentialSource, parseCredentialsJsonString, resolveAuthLoginConfig, } from './auth-login-resolve.js';
|
|
14
14
|
import { runAutomatedAuthLogin } from './auth-login-run.js';
|
|
15
15
|
const program = new Command();
|
|
@@ -33,11 +33,14 @@ function redactConfigForLog(config) {
|
|
|
33
33
|
base.auth = {
|
|
34
34
|
...config.auth,
|
|
35
35
|
credentials: {
|
|
36
|
-
username:
|
|
36
|
+
username: '***',
|
|
37
37
|
password: '***',
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
if (config.auth?.type === 'storage-state') {
|
|
42
|
+
base.auth = { type: 'storage-state', path: '<provided>' };
|
|
43
|
+
}
|
|
41
44
|
return base;
|
|
42
45
|
}
|
|
43
46
|
function mergeAuthFromCli(config, options) {
|
|
@@ -95,6 +98,7 @@ async function runAnalyze(options) {
|
|
|
95
98
|
repoPath: options.repo,
|
|
96
99
|
config,
|
|
97
100
|
writeArtifacts,
|
|
101
|
+
skipAuthDetection: options.skipAuthDetection,
|
|
98
102
|
});
|
|
99
103
|
if (ephemeral) {
|
|
100
104
|
console.log(JSON.stringify({
|
|
@@ -154,6 +158,7 @@ program
|
|
|
154
158
|
.option('--config <file>', 'Path to config file (relative to cwd)', 'qulib.config.ts')
|
|
155
159
|
.option('--adapter <type>', 'Override default test adapter (playwright, cypress-e2e, cypress-component, api)', 'playwright')
|
|
156
160
|
.option('--ephemeral', 'Do not write to disk — return full report as JSON on stdout (use for MCP/CI)', false)
|
|
161
|
+
.option('--skip-auth-detection', 'Crawl the public surface even if auth is detected (useful for sites with sign-in CTAs on public pages)', false)
|
|
157
162
|
.option('--auth-storage-state <path>', 'Path to a storage state JSON file (use after `qulib auth init`)')
|
|
158
163
|
.option('--auth-form-login', 'Use form-login; requires --login-url, credentials, and selectors', false)
|
|
159
164
|
.option('--login-url <url>', 'Form login page URL (required with --auth-form-login)')
|
|
@@ -176,6 +181,7 @@ program
|
|
|
176
181
|
repo: options.repo,
|
|
177
182
|
configFile: options.config,
|
|
178
183
|
ephemeral: options.ephemeral,
|
|
184
|
+
skipAuthDetection: Boolean(options.skipAuthDetection),
|
|
179
185
|
authStorageState: options.authStorageState,
|
|
180
186
|
authFormLogin,
|
|
181
187
|
loginUrl,
|
|
@@ -212,7 +218,7 @@ providersCmd
|
|
|
212
218
|
.command('list')
|
|
213
219
|
.description('List user-local providers registered on this machine')
|
|
214
220
|
.action(async () => {
|
|
215
|
-
const { listUserProviders } = await import('../tools/
|
|
221
|
+
const { listUserProviders } = await import('../tools/auth/custom-providers.js');
|
|
216
222
|
const providers = listUserProviders();
|
|
217
223
|
console.log(JSON.stringify(providers, null, 2));
|
|
218
224
|
});
|
|
@@ -229,7 +235,7 @@ providersCmd
|
|
|
229
235
|
catch {
|
|
230
236
|
throw new Error(`Invalid regex pattern: ${opts.pattern}`);
|
|
231
237
|
}
|
|
232
|
-
const { addUserProvider } = await import('../tools/
|
|
238
|
+
const { addUserProvider } = await import('../tools/auth/custom-providers.js');
|
|
233
239
|
addUserProvider({ id: opts.id, label: opts.label, pattern: opts.pattern });
|
|
234
240
|
console.log(`[qulib] Added provider "${opts.label}" (id: ${opts.id}) to ~/.qulib/providers.json`);
|
|
235
241
|
});
|
|
@@ -238,7 +244,7 @@ providersCmd
|
|
|
238
244
|
.description('Remove a user-local provider by id')
|
|
239
245
|
.requiredOption('--id <id>', 'Provider id to remove')
|
|
240
246
|
.action(async (opts) => {
|
|
241
|
-
const { removeUserProvider } = await import('../tools/
|
|
247
|
+
const { removeUserProvider } = await import('../tools/auth/custom-providers.js');
|
|
242
248
|
const removed = removeUserProvider(opts.id);
|
|
243
249
|
console.log(removed ? `[qulib] Removed "${opts.id}"` : `[qulib] No provider with id "${opts.id}" found`);
|
|
244
250
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { analyzeApp } from './analyze.js';
|
|
2
|
-
export { detectAuth } from './tools/auth
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
2
|
+
export { detectAuth, validateStorageState, evaluateStorageStateValidity, preflightStorageStateFile, waitForReturnToOrigin, } from './tools/auth/detect.js';
|
|
3
|
+
export type { StorageStateInvalidReason, StorageStateValidationResult, } from './tools/auth/detect.js';
|
|
4
|
+
export { exploreAuth } from './tools/auth/explore.js';
|
|
5
|
+
export { addUserProvider, removeUserProvider, listUserProviders } from './tools/auth/custom-providers.js';
|
|
6
|
+
export { scanRepo } from './tools/repo/scan.js';
|
|
7
|
+
export { computeAutomationMaturity } from './tools/scoring/automation-maturity.js';
|
|
7
8
|
export { createProvider } from './llm/provider-registry.js';
|
|
8
9
|
export { resolveMaxOutputTokensPerLlmCall } from './schemas/config.schema.js';
|
|
9
10
|
export { resolveScanStateBaseDir, resolveReportDir } from './harness/state-manager.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1G,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,yBAAyB,EAAE,MAAM,wCAAwC,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACvF,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjF,YAAY,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,YAAY,EACV,aAAa,EACb,cAAc,EACd,kBAAkB,GACnB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC9E,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,EACb,kBAAkB,EAClB,2BAA2B,EAC3B,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
|