@qulib/core 0.4.2 → 0.4.3
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 +34 -8
- 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 +9 -6
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/phases/observe.js +2 -2
- package/dist/phases/think.js +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 +219 -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
|
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) {
|
|
@@ -212,7 +215,7 @@ providersCmd
|
|
|
212
215
|
.command('list')
|
|
213
216
|
.description('List user-local providers registered on this machine')
|
|
214
217
|
.action(async () => {
|
|
215
|
-
const { listUserProviders } = await import('../tools/
|
|
218
|
+
const { listUserProviders } = await import('../tools/auth/custom-providers.js');
|
|
216
219
|
const providers = listUserProviders();
|
|
217
220
|
console.log(JSON.stringify(providers, null, 2));
|
|
218
221
|
});
|
|
@@ -229,7 +232,7 @@ providersCmd
|
|
|
229
232
|
catch {
|
|
230
233
|
throw new Error(`Invalid regex pattern: ${opts.pattern}`);
|
|
231
234
|
}
|
|
232
|
-
const { addUserProvider } = await import('../tools/
|
|
235
|
+
const { addUserProvider } = await import('../tools/auth/custom-providers.js');
|
|
233
236
|
addUserProvider({ id: opts.id, label: opts.label, pattern: opts.pattern });
|
|
234
237
|
console.log(`[qulib] Added provider "${opts.label}" (id: ${opts.id}) to ~/.qulib/providers.json`);
|
|
235
238
|
});
|
|
@@ -238,7 +241,7 @@ providersCmd
|
|
|
238
241
|
.description('Remove a user-local provider by id')
|
|
239
242
|
.requiredOption('--id <id>', 'Provider id to remove')
|
|
240
243
|
.action(async (opts) => {
|
|
241
|
-
const { removeUserProvider } = await import('../tools/
|
|
244
|
+
const { removeUserProvider } = await import('../tools/auth/custom-providers.js');
|
|
242
245
|
const removed = removeUserProvider(opts.id);
|
|
243
246
|
console.log(removed ? `[qulib] Removed "${opts.id}"` : `[qulib] No provider with id "${opts.id}" found`);
|
|
244
247
|
});
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { analyzeApp } from './analyze.js';
|
|
2
|
-
export { detectAuth } from './tools/auth
|
|
3
|
-
export { exploreAuth } from './tools/auth
|
|
4
|
-
export { addUserProvider, removeUserProvider, listUserProviders } from './tools/
|
|
5
|
-
export { scanRepo } from './tools/repo
|
|
6
|
-
export { computeAutomationMaturity } from './tools/automation-maturity.js';
|
|
2
|
+
export { detectAuth, validateStorageState, evaluateStorageStateValidity, preflightStorageStateFile, waitForReturnToOrigin, } from './tools/auth/detect.js';
|
|
3
|
+
export { exploreAuth } from './tools/auth/explore.js';
|
|
4
|
+
export { addUserProvider, removeUserProvider, listUserProviders } from './tools/auth/custom-providers.js';
|
|
5
|
+
export { scanRepo } from './tools/repo/scan.js';
|
|
6
|
+
export { computeAutomationMaturity } from './tools/scoring/automation-maturity.js';
|
|
7
7
|
export { createProvider } from './llm/provider-registry.js';
|
|
8
8
|
export { resolveMaxOutputTokensPerLlmCall } from './schemas/config.schema.js';
|
|
9
9
|
export { resolveScanStateBaseDir, resolveReportDir } from './harness/state-manager.js';
|
package/dist/phases/observe.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RouteInventorySchema } from '../schemas/route-inventory.schema.js';
|
|
2
2
|
import { RepoAnalysisSchema } from '../schemas/repo-analysis.schema.js';
|
|
3
|
-
import { createExplorer } from '../tools/
|
|
4
|
-
import { scanRepo } from '../tools/repo
|
|
3
|
+
import { createExplorer } from '../tools/explorers/factory.js';
|
|
4
|
+
import { scanRepo } from '../tools/repo/scan.js';
|
|
5
5
|
import { StateManager } from '../harness/state-manager.js';
|
|
6
6
|
import { logDecision } from '../harness/decision-logger.js';
|
|
7
7
|
import { emitTelemetry, redactUrlForTelemetry } from '../telemetry/emit.js';
|
package/dist/phases/think.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GapAnalysisSchema } from '../schemas/gap-analysis.schema.js';
|
|
2
|
-
import { analyzeGaps } from '../tools/
|
|
2
|
+
import { analyzeGaps } from '../tools/scoring/gaps.js';
|
|
3
3
|
import { logDecision } from '../harness/decision-logger.js';
|
|
4
4
|
import { finalizeGapAnalysisFromDraft } from './think-finalize.js';
|
|
5
5
|
import { emitTelemetry } from '../telemetry/emit.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type TelemetryEventKind = 'scan.started' | 'scan.completed' | 'scan.blocked' | 'phase.observe.started' | 'phase.observe.completed' | 'phase.think.started' | 'phase.think.completed' | 'phase.act.started' | 'phase.act.completed' | 'llm.call.started' | 'llm.call.completed' | 'llm.call.failed' | 'gap.detected' | 'auth.detected' | 'repo.scanned';
|
|
1
|
+
export type TelemetryEventKind = 'scan.started' | 'scan.completed' | 'scan.blocked' | 'phase.observe.started' | 'phase.observe.completed' | 'phase.think.started' | 'phase.think.completed' | 'phase.act.started' | 'phase.act.completed' | 'llm.call.started' | 'llm.call.completed' | 'llm.call.failed' | 'gap.detected' | 'auth.detected' | 'auth.storage-state.validated' | 'repo.scanned';
|
|
2
2
|
export interface TelemetryEvent {
|
|
3
3
|
kind: TelemetryEventKind;
|
|
4
4
|
timestamp: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry.interface.d.ts","sourceRoot":"","sources":["../../src/telemetry/telemetry.interface.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAC1B,cAAc,GACd,gBAAgB,GAChB,cAAc,GACd,uBAAuB,GACvB,yBAAyB,GACzB,qBAAqB,GACrB,uBAAuB,GACvB,mBAAmB,GACnB,qBAAqB,GACrB,kBAAkB,GAClB,oBAAoB,GACpB,iBAAiB,GACjB,cAAc,GACd,eAAe,GACf,cAAc,CAAC;AAEnB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;CACnC;AAED,eAAO,MAAM,iBAAiB,EAAE,aAE/B,CAAC"}
|
|
1
|
+
{"version":3,"file":"telemetry.interface.d.ts","sourceRoot":"","sources":["../../src/telemetry/telemetry.interface.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAC1B,cAAc,GACd,gBAAgB,GAChB,cAAc,GACd,uBAAuB,GACvB,yBAAyB,GACzB,qBAAqB,GACrB,uBAAuB,GACvB,mBAAmB,GACnB,qBAAqB,GACrB,kBAAkB,GAClB,oBAAoB,GACpB,iBAAiB,GACjB,cAAc,GACd,eAAe,GACf,8BAA8B,GAC9B,cAAc,CAAC;AAEnB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;CACnC;AAED,eAAO,MAAM,iBAAiB,EAAE,aAE/B,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Browser, BrowserContext } from '@playwright/test';
|
|
2
|
+
import type { AuthConfig } from '../schemas/config.schema.js';
|
|
3
|
+
export declare function createAuthenticatedContext(browser: Browser, auth: AuthConfig | undefined, timeoutMs: number): Promise<BrowserContext>;
|
|
4
|
+
//# sourceMappingURL=apply-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-auth.d.ts","sourceRoot":"","sources":["../../src/tools/apply-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEhE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,UAAU,GAAG,SAAS,EAC5B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC,CAsCzB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
export async function createAuthenticatedContext(browser, auth, timeoutMs) {
|
|
3
|
+
if (!auth) {
|
|
4
|
+
return browser.newContext();
|
|
5
|
+
}
|
|
6
|
+
if (auth.type === 'storage-state') {
|
|
7
|
+
const storagePath = resolve(process.cwd(), auth.path);
|
|
8
|
+
return browser.newContext({ storageState: storagePath });
|
|
9
|
+
}
|
|
10
|
+
const context = await browser.newContext();
|
|
11
|
+
const page = await context.newPage();
|
|
12
|
+
try {
|
|
13
|
+
await page.goto(auth.loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
14
|
+
await page.fill(auth.selectors.username, auth.credentials.username);
|
|
15
|
+
await page.fill(auth.selectors.password, auth.credentials.password);
|
|
16
|
+
await page.click(auth.selectors.submit);
|
|
17
|
+
const urlFragment = auth.successIndicator.urlContains;
|
|
18
|
+
if (urlFragment) {
|
|
19
|
+
await page.waitForURL((url) => url.toString().includes(urlFragment), {
|
|
20
|
+
timeout: timeoutMs,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const visibleSelector = auth.successIndicator.selectorVisible;
|
|
24
|
+
if (visibleSelector) {
|
|
25
|
+
await page.waitForSelector(visibleSelector, {
|
|
26
|
+
timeout: timeoutMs,
|
|
27
|
+
state: 'visible',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await page.close();
|
|
33
|
+
}
|
|
34
|
+
return context;
|
|
35
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Browser, BrowserContext } from '@playwright/test';
|
|
2
|
+
import type { AuthConfig } from '../../schemas/config.schema.js';
|
|
3
|
+
export declare function createAuthenticatedContext(browser: Browser, auth: AuthConfig | undefined, timeoutMs: number): Promise<BrowserContext>;
|
|
4
|
+
//# sourceMappingURL=apply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/apply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEhE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAEjE,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,UAAU,GAAG,SAAS,EAC5B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC,CAsCzB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
export async function createAuthenticatedContext(browser, auth, timeoutMs) {
|
|
3
|
+
if (!auth) {
|
|
4
|
+
return browser.newContext();
|
|
5
|
+
}
|
|
6
|
+
if (auth.type === 'storage-state') {
|
|
7
|
+
const storagePath = resolve(process.cwd(), auth.path);
|
|
8
|
+
return browser.newContext({ storageState: storagePath });
|
|
9
|
+
}
|
|
10
|
+
const context = await browser.newContext();
|
|
11
|
+
const page = await context.newPage();
|
|
12
|
+
try {
|
|
13
|
+
await page.goto(auth.loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
14
|
+
await page.fill(auth.selectors.username, auth.credentials.username);
|
|
15
|
+
await page.fill(auth.selectors.password, auth.credentials.password);
|
|
16
|
+
await page.click(auth.selectors.submit);
|
|
17
|
+
const urlFragment = auth.successIndicator.urlContains;
|
|
18
|
+
if (urlFragment) {
|
|
19
|
+
await page.waitForURL((url) => url.toString().includes(urlFragment), {
|
|
20
|
+
timeout: timeoutMs,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const visibleSelector = auth.successIndicator.selectorVisible;
|
|
24
|
+
if (visibleSelector) {
|
|
25
|
+
await page.waitForSelector(visibleSelector, {
|
|
26
|
+
timeout: timeoutMs,
|
|
27
|
+
state: 'visible',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await page.close();
|
|
33
|
+
}
|
|
34
|
+
return context;
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Gap } from '../../schemas/gap-analysis.schema.js';
|
|
2
|
+
import type { StorageStateInvalidReason } from './detector.js';
|
|
3
|
+
export declare function buildAuthBlockGap(url: string): Gap;
|
|
4
|
+
export declare function buildStorageStateInvalidGap(input: {
|
|
5
|
+
url: string;
|
|
6
|
+
reasonCode: StorageStateInvalidReason;
|
|
7
|
+
reason: string;
|
|
8
|
+
}): Gap;
|
|
9
|
+
//# sourceMappingURL=block-gap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-gap.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/block-gap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,sCAAsC,CAAC;AAChE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAmB/D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAYlD;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,yBAAyB,CAAC;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,GAAG,CAqBN"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function safeOriginAndPath(url) {
|
|
2
|
+
try {
|
|
3
|
+
const u = new URL(url);
|
|
4
|
+
return `${u.origin}${u.pathname}`;
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return url;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function safeHost(url) {
|
|
11
|
+
try {
|
|
12
|
+
return new URL(url).hostname;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return url;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function buildAuthBlockGap(url) {
|
|
19
|
+
const host = safeHost(url);
|
|
20
|
+
const safeUrl = safeOriginAndPath(url);
|
|
21
|
+
return {
|
|
22
|
+
id: 'auth-block',
|
|
23
|
+
path: '/',
|
|
24
|
+
severity: 'critical',
|
|
25
|
+
category: 'coverage',
|
|
26
|
+
reason: `Scan blocked by authentication. No authenticated pages were evaluated for ${host}.`,
|
|
27
|
+
description: 'Scan blocked by authentication. 0 authenticated pages were evaluated.',
|
|
28
|
+
recommendation: `Run \`qulib auth init --base-url ${safeUrl}\` to capture a storage state, then re-run with --auth storage-state.`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function buildStorageStateInvalidGap(input) {
|
|
32
|
+
const host = safeHost(input.url);
|
|
33
|
+
const safeUrl = safeOriginAndPath(input.url);
|
|
34
|
+
const recoveryByCode = {
|
|
35
|
+
'missing-file': `Storage state file was not found. Run \`qulib auth login --base-url ${safeUrl} --out <path>\` (or \`qulib auth init\`) to capture a fresh state, then re-run \`qulib analyze --url ${safeUrl} --auth-storage-state <path>\`.`,
|
|
36
|
+
'unreadable-file': `Storage state file exists but could not be read. Check file permissions, then re-run \`qulib auth login\` if needed.`,
|
|
37
|
+
'invalid-json': `Storage state file is not valid JSON. Run \`qulib auth login --base-url ${safeUrl} --out <path>\` again to regenerate it.`,
|
|
38
|
+
'wrong-origin': `Storage state belongs to a different origin than ${host}. Re-run \`qulib auth login --base-url ${safeUrl}\` against this target and pass the new file to \`qulib analyze\`.`,
|
|
39
|
+
'expired-or-unauthorized': `The session in the storage state has expired or is unauthorized. Run \`qulib auth login --base-url ${safeUrl}\` to capture a fresh state, then re-run \`qulib analyze --url ${safeUrl} --auth-storage-state <path>\`.`,
|
|
40
|
+
'no-auth-cookies': `Storage state file contains no cookies or localStorage entries — it is effectively empty. Run \`qulib auth login --base-url ${safeUrl}\` to capture a real session.`,
|
|
41
|
+
unknown: `Storage state could not be validated. Try \`qulib auth login --base-url ${safeUrl}\` again, and verify the file was saved on the same origin.`,
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
id: 'storage-state-invalid',
|
|
45
|
+
path: '/',
|
|
46
|
+
severity: 'critical',
|
|
47
|
+
category: 'coverage',
|
|
48
|
+
reason: `Authenticated scan could not continue because the provided storage state is invalid for ${host}. Reason: ${input.reasonCode} — ${input.reason}.`,
|
|
49
|
+
description: `Storage state validation failed before crawling. The session was checked against ${host} and rejected with reason code "${input.reasonCode}".`,
|
|
50
|
+
recommendation: recoveryByCode[input.reasonCode],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { OAuthProvider } from './providers.js';
|
|
2
|
+
export interface SerializedProvider {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
patterns: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function loadUserProviders(): OAuthProvider[];
|
|
8
|
+
export declare function addUserProvider(input: {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
pattern: string;
|
|
12
|
+
}): void;
|
|
13
|
+
export declare function removeUserProvider(id: string): boolean;
|
|
14
|
+
export declare function listUserProviders(): SerializedProvider[];
|
|
15
|
+
//# sourceMappingURL=custom-providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-providers.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/custom-providers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIpD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,iBAAiB,IAAI,aAAa,EAAE,CAOnD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAc3F;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAStD;AAED,wBAAgB,iBAAiB,IAAI,kBAAkB,EAAE,CAExD"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
const USER_PROVIDERS_PATH = join(homedir(), '.qulib', 'providers.json');
|
|
5
|
+
export function loadUserProviders() {
|
|
6
|
+
const raw = loadSerialized();
|
|
7
|
+
return raw.map((p) => ({
|
|
8
|
+
id: p.id,
|
|
9
|
+
label: p.label,
|
|
10
|
+
patterns: p.patterns.map((src) => new RegExp(src, 'i')),
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
export function addUserProvider(input) {
|
|
14
|
+
const existing = loadSerialized();
|
|
15
|
+
const idx = existing.findIndex((p) => p.id === input.id);
|
|
16
|
+
if (idx >= 0) {
|
|
17
|
+
const p = existing[idx];
|
|
18
|
+
if (!p.patterns.includes(input.pattern)) {
|
|
19
|
+
p.patterns.push(input.pattern);
|
|
20
|
+
}
|
|
21
|
+
p.label = input.label;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
existing.push({ id: input.id, label: input.label, patterns: [input.pattern] });
|
|
25
|
+
}
|
|
26
|
+
ensureDir();
|
|
27
|
+
writeFileSync(USER_PROVIDERS_PATH, JSON.stringify(existing, null, 2), 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
export function removeUserProvider(id) {
|
|
30
|
+
const existing = loadSerialized();
|
|
31
|
+
const filtered = existing.filter((p) => p.id !== id);
|
|
32
|
+
if (filtered.length === existing.length) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
ensureDir();
|
|
36
|
+
writeFileSync(USER_PROVIDERS_PATH, JSON.stringify(filtered, null, 2), 'utf-8');
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
export function listUserProviders() {
|
|
40
|
+
return loadSerialized();
|
|
41
|
+
}
|
|
42
|
+
function loadSerialized() {
|
|
43
|
+
if (!existsSync(USER_PROVIDERS_PATH)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(readFileSync(USER_PROVIDERS_PATH, 'utf-8'));
|
|
48
|
+
if (!Array.isArray(parsed)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function ensureDir() {
|
|
58
|
+
const dir = dirname(USER_PROVIDERS_PATH);
|
|
59
|
+
if (!existsSync(dir)) {
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
2
|
+
import type { DetectedAuth } from '../../schemas/config.schema.js';
|
|
3
|
+
import type { AnalyzeProgressSink } from '../../harness/progress-log.js';
|
|
4
|
+
export type StorageStateInvalidReason = 'missing-file' | 'unreadable-file' | 'invalid-json' | 'wrong-origin' | 'expired-or-unauthorized' | 'no-auth-cookies' | 'unknown';
|
|
5
|
+
export interface StorageStateValidationResult {
|
|
6
|
+
valid: boolean;
|
|
7
|
+
reasonCode: StorageStateInvalidReason | 'ok';
|
|
8
|
+
reason: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function evaluateStorageStateValidity(signals: {
|
|
11
|
+
expectedOrigin: string;
|
|
12
|
+
finalUrl: string;
|
|
13
|
+
visiblePasswordCount: number;
|
|
14
|
+
hadUnauthorizedHttp: boolean;
|
|
15
|
+
}): StorageStateValidationResult;
|
|
16
|
+
export declare function preflightStorageStateFile(storagePath: string): Promise<StorageStateValidationResult | null>;
|
|
17
|
+
export declare function waitForReturnToOrigin(page: Page, baseUrl: string, timeoutMs?: number): Promise<{
|
|
18
|
+
returned: boolean;
|
|
19
|
+
finalUrl: string;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function validateStorageState(url: string, storagePath: string, timeoutMs?: number): Promise<StorageStateValidationResult>;
|
|
22
|
+
export declare function detectAuth(url: string, timeoutMs?: number, progress?: AnalyzeProgressSink): Promise<DetectedAuth>;
|
|
23
|
+
//# sourceMappingURL=detect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/detect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAY,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAIzE,MAAM,MAAM,yBAAyB,GACjC,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,cAAc,GACd,yBAAyB,GACzB,iBAAiB,GACjB,SAAS,CAAC;AAEd,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,yBAAyB,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB;AAsQD,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACpD,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,OAAO,CAAC;CAC9B,GAAG,4BAA4B,CA6B/B;AAOD,wBAAsB,yBAAyB,CAC7C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC,CAgD9C;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,EACf,SAAS,SAAQ,GAChB,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAelD;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,SAAS,SAAQ,GAChB,OAAO,CAAC,4BAA4B,CAAC,CAyDvC;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAqJvB"}
|