@qulib/core 0.5.2 → 0.6.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.
Files changed (102) hide show
  1. package/README.md +9 -9
  2. package/dist/agent-summary.d.ts +52 -0
  3. package/dist/agent-summary.d.ts.map +1 -0
  4. package/dist/agent-summary.js +187 -0
  5. package/dist/cli/index.js +18 -4
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/tools/auth/detect.d.ts +8 -0
  10. package/dist/tools/auth/detect.d.ts.map +1 -1
  11. package/dist/tools/auth/detect.js +33 -9
  12. package/package.json +2 -2
  13. package/dist/tools/apply-auth.d.ts +0 -4
  14. package/dist/tools/apply-auth.d.ts.map +0 -1
  15. package/dist/tools/apply-auth.js +0 -35
  16. package/dist/tools/auth/block-gap.d.ts +0 -9
  17. package/dist/tools/auth/block-gap.d.ts.map +0 -1
  18. package/dist/tools/auth/block-gap.js +0 -52
  19. package/dist/tools/auth/detector.d.ts +0 -23
  20. package/dist/tools/auth/detector.d.ts.map +0 -1
  21. package/dist/tools/auth/detector.js +0 -526
  22. package/dist/tools/auth/explorer.d.ts +0 -4
  23. package/dist/tools/auth/explorer.d.ts.map +0 -1
  24. package/dist/tools/auth/explorer.js +0 -346
  25. package/dist/tools/auth/oauth-providers.d.ts +0 -7
  26. package/dist/tools/auth/oauth-providers.d.ts.map +0 -1
  27. package/dist/tools/auth/oauth-providers.js +0 -21
  28. package/dist/tools/auth/surface-analyzer.d.ts +0 -4
  29. package/dist/tools/auth/surface-analyzer.d.ts.map +0 -1
  30. package/dist/tools/auth/surface-analyzer.js +0 -170
  31. package/dist/tools/auth/user-providers.d.ts +0 -15
  32. package/dist/tools/auth/user-providers.d.ts.map +0 -1
  33. package/dist/tools/auth/user-providers.js +0 -62
  34. package/dist/tools/auth-block-gap.d.ts +0 -9
  35. package/dist/tools/auth-block-gap.d.ts.map +0 -1
  36. package/dist/tools/auth-block-gap.js +0 -52
  37. package/dist/tools/auth-detector.d.ts +0 -23
  38. package/dist/tools/auth-detector.d.ts.map +0 -1
  39. package/dist/tools/auth-detector.js +0 -526
  40. package/dist/tools/auth-explorer.d.ts +0 -4
  41. package/dist/tools/auth-explorer.d.ts.map +0 -1
  42. package/dist/tools/auth-explorer.js +0 -346
  43. package/dist/tools/auth-surface-analyzer.d.ts +0 -4
  44. package/dist/tools/auth-surface-analyzer.d.ts.map +0 -1
  45. package/dist/tools/auth-surface-analyzer.js +0 -170
  46. package/dist/tools/auth.d.ts +0 -4
  47. package/dist/tools/auth.d.ts.map +0 -1
  48. package/dist/tools/auth.js +0 -35
  49. package/dist/tools/automation-maturity.d.ts +0 -4
  50. package/dist/tools/automation-maturity.d.ts.map +0 -1
  51. package/dist/tools/automation-maturity.js +0 -219
  52. package/dist/tools/browser.d.ts +0 -3
  53. package/dist/tools/browser.d.ts.map +0 -1
  54. package/dist/tools/browser.js +0 -13
  55. package/dist/tools/cypress-explorer.d.ts +0 -8
  56. package/dist/tools/cypress-explorer.d.ts.map +0 -1
  57. package/dist/tools/cypress-explorer.js +0 -5
  58. package/dist/tools/explorer-factory.d.ts +0 -4
  59. package/dist/tools/explorer-factory.d.ts.map +0 -1
  60. package/dist/tools/explorer-factory.js +0 -12
  61. package/dist/tools/explorer.interface.d.ts +0 -7
  62. package/dist/tools/explorer.interface.d.ts.map +0 -1
  63. package/dist/tools/explorer.interface.js +0 -1
  64. package/dist/tools/explorers/cypress-explorer.d.ts +0 -8
  65. package/dist/tools/explorers/cypress-explorer.d.ts.map +0 -1
  66. package/dist/tools/explorers/cypress-explorer.js +0 -5
  67. package/dist/tools/explorers/explorer.interface.d.ts +0 -7
  68. package/dist/tools/explorers/explorer.interface.d.ts.map +0 -1
  69. package/dist/tools/explorers/explorer.interface.js +0 -1
  70. package/dist/tools/explorers/playwright-explorer.d.ts +0 -8
  71. package/dist/tools/explorers/playwright-explorer.d.ts.map +0 -1
  72. package/dist/tools/explorers/playwright-explorer.js +0 -172
  73. package/dist/tools/framework-detector.d.ts +0 -15
  74. package/dist/tools/framework-detector.d.ts.map +0 -1
  75. package/dist/tools/framework-detector.js +0 -153
  76. package/dist/tools/gap-engine.d.ts +0 -8
  77. package/dist/tools/gap-engine.d.ts.map +0 -1
  78. package/dist/tools/gap-engine.js +0 -138
  79. package/dist/tools/oauth-providers.d.ts +0 -7
  80. package/dist/tools/oauth-providers.d.ts.map +0 -1
  81. package/dist/tools/oauth-providers.js +0 -21
  82. package/dist/tools/playwright-explorer.d.ts +0 -8
  83. package/dist/tools/playwright-explorer.d.ts.map +0 -1
  84. package/dist/tools/playwright-explorer.js +0 -172
  85. package/dist/tools/public-surface.d.ts +0 -5
  86. package/dist/tools/public-surface.d.ts.map +0 -1
  87. package/dist/tools/public-surface.js +0 -13
  88. package/dist/tools/repo/framework-detector.d.ts +0 -15
  89. package/dist/tools/repo/framework-detector.d.ts.map +0 -1
  90. package/dist/tools/repo/framework-detector.js +0 -153
  91. package/dist/tools/repo/scanner.d.ts +0 -19
  92. package/dist/tools/repo/scanner.d.ts.map +0 -1
  93. package/dist/tools/repo/scanner.js +0 -181
  94. package/dist/tools/repo-scanner.d.ts +0 -19
  95. package/dist/tools/repo-scanner.d.ts.map +0 -1
  96. package/dist/tools/repo-scanner.js +0 -181
  97. package/dist/tools/scoring/gap-engine.d.ts +0 -8
  98. package/dist/tools/scoring/gap-engine.d.ts.map +0 -1
  99. package/dist/tools/scoring/gap-engine.js +0 -138
  100. package/dist/tools/user-providers.d.ts +0 -15
  101. package/dist/tools/user-providers.d.ts.map +0 -1
  102. package/dist/tools/user-providers.js +0 -62
package/README.md CHANGED
@@ -49,13 +49,13 @@ This opens a real browser. Log in normally (OAuth, magic link, password manager,
49
49
 
50
50
  ### Automated form login (`auth login`)
51
51
 
52
- When **`detect-auth`** shows **`authOptions`** with **`type: "form-login"`** and **`requirements.method: "credentials"`** (including click-to-reveal paths such as Scholastic Sync), you can save a storage state **without** manual clicking:
52
+ When **`detect-auth`** shows **`authOptions`** with **`type: "form-login"`** and **`requirements.method: "credentials"`** (including click-to-reveal paths such as NQ Login), you can save a storage state **without** manual clicking:
53
53
 
54
54
  ```bash
55
- qulib auth login --base-url https://platform.scholastic.com \
56
- --auth-path scholastic-sync \
57
- --credentials-file ~/.qulib/scholastic-creds.json \
58
- --out ~/.qulib/scholastic-state.json
55
+ qulib auth login --base-url https://notquality.com \
56
+ --auth-path nq-login \
57
+ --credentials-file ~/.qulib/nq-creds.json \
58
+ --out ~/.qulib/nq-state.json
59
59
  ```
60
60
 
61
61
  The JSON file must map **field `name`** values from `authOptions` to secrets, e.g. `{"username":"…","password":"…","hidden.datasource":"…"}`. Prefer **`--credentials-file`** over **`--credentials`** so values are not stored in shell history.
@@ -63,8 +63,8 @@ The JSON file must map **field `name`** values from `authOptions` to secrets, e.
63
63
  Then analyze with the saved session:
64
64
 
65
65
  ```bash
66
- qulib analyze --url https://platform.scholastic.com \
67
- --auth-storage-state ~/.qulib/scholastic-state.json
66
+ qulib analyze --url https://notquality.com \
67
+ --auth-storage-state ~/.qulib/nq-state.json
68
68
  ```
69
69
 
70
70
  Use **`--auth-path <id>`** when multiple **`form-login`** paths appear in **`authOptions`**. Use **`--success-url-contains <substring>`** for stricter success detection; otherwise Qulib infers success from URL changes or the password field disappearing (and warns if it cannot confirm).
@@ -102,9 +102,9 @@ For unfamiliar apps (especially enterprise SSO with several buttons), run **`qul
102
102
  Unknown SSO buttons include **`unrecognizedButtons`** with a hint. Teach this machine to recognize a label next time:
103
103
 
104
104
  ```bash
105
- qulib auth providers add --id scholastic-sync --label "Scholastic Sync" --pattern "scholastic sync"
105
+ qulib auth providers add --id nq-login --label "NQ Login" --pattern "nq login"
106
106
  qulib auth providers list
107
- qulib auth providers remove --id scholastic-sync
107
+ qulib auth providers remove --id nq-login
108
108
  ```
109
109
 
110
110
  Patterns live in **`~/.qulib/providers.json`** (per user, not in the repo). Built-in public platforms stay in qulib’s curated list; tenant-specific names are never shipped as built-ins.
@@ -0,0 +1,52 @@
1
+ import type { AnalyzeResult } from './analyze.js';
2
+ import type { CostIntelligence } from './schemas/cost-intelligence.schema.js';
3
+ /**
4
+ * Agent-readable summary of an AnalyzeResult. Intended for orchestrators
5
+ * (CI gates, external agent loops) that need a small, stable JSON shape.
6
+ *
7
+ * QLIB-001 spec: see docs/agent-summary-output.md.
8
+ * C02 helper; exposed via CLI (`--agent-summary`) and MCP (`agentSummary: true`).
9
+ * Field names below are the v1 contract for `toAgentSummary`.
10
+ */
11
+ export type AgentGate = 'pass' | 'warn' | 'fail';
12
+ export type CoverageStatus = 'ok' | 'thin' | 'blocked-by-auth' | 'budget-exceeded' | 'navigation-failures' | 'unknown';
13
+ export interface AgentSummaryCostSummary {
14
+ maxOutputTokensPerLlmCall: number;
15
+ totalInputTokens: number;
16
+ totalOutputTokens: number;
17
+ budgetWarningCount: number;
18
+ dataQuality: CostIntelligence['usageSummary']['dataQuality'];
19
+ maturityLevel: number;
20
+ maturityLabel: string;
21
+ }
22
+ export interface AgentSummary {
23
+ schemaVersion: 1;
24
+ gate: AgentGate;
25
+ releaseConfidence: number | null;
26
+ coverageStatus: CoverageStatus;
27
+ topRisks: string[];
28
+ recommendedNextChecks: string[];
29
+ honestyNotes: string[];
30
+ costSummary: AgentSummaryCostSummary | null;
31
+ deterministicFollowUps: string[];
32
+ }
33
+ export interface AgentSummaryPolicy {
34
+ /** Confidence at or above this number is required for `pass`. Default 80. */
35
+ passConfidenceThreshold?: number;
36
+ /** Confidence below this triggers `fail` (when no harder failure already applies). Default 30. */
37
+ failConfidenceThreshold?: number;
38
+ /** Max risks/checks/notes to include in each list. Default 5. */
39
+ maxListLength?: number;
40
+ /**
41
+ * Treat `mode === 'auth-required'` as `fail` (default) or `warn`.
42
+ * Honest default: a deployment that was never exercised past auth cannot pass.
43
+ */
44
+ authRequiredGate?: 'fail' | 'warn';
45
+ }
46
+ /**
47
+ * Convert an `AnalyzeResult` into a small agent-facing summary.
48
+ *
49
+ * Pure function. No I/O. CLI / MCP wiring belongs to QLIB-001-C03 and -C04.
50
+ */
51
+ export declare function toAgentSummary(result: AnalyzeResult, policy?: AgentSummaryPolicy): AgentSummary;
52
+ //# sourceMappingURL=agent-summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-summary.d.ts","sourceRoot":"","sources":["../src/agent-summary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;;;;;GAOG;AAEH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjD,MAAM,MAAM,cAAc,GACtB,IAAI,GACJ,MAAM,GACN,iBAAiB,GACjB,iBAAiB,GACjB,qBAAqB,GACrB,SAAS,CAAC;AAEd,MAAM,WAAW,uBAAuB;IACtC,yBAAyB,EAAE,MAAM,CAAC;IAClC,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAC,aAAa,CAAC,CAAC;IAC7D,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,CAAC,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;IAChB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAC5C,sBAAsB,EAAE,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,kGAAkG;IAClG,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACpC;AAmLD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,MAAM,CAAC,EAAE,kBAAkB,GAC1B,YAAY,CAiBd"}
@@ -0,0 +1,187 @@
1
+ const DEFAULT_POLICY = {
2
+ passConfidenceThreshold: 80,
3
+ failConfidenceThreshold: 30,
4
+ maxListLength: 5,
5
+ authRequiredGate: 'fail',
6
+ };
7
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
8
+ function resolvePolicy(p) {
9
+ return {
10
+ passConfidenceThreshold: p?.passConfidenceThreshold ?? DEFAULT_POLICY.passConfidenceThreshold,
11
+ failConfidenceThreshold: p?.failConfidenceThreshold ?? DEFAULT_POLICY.failConfidenceThreshold,
12
+ maxListLength: p?.maxListLength ?? DEFAULT_POLICY.maxListLength,
13
+ authRequiredGate: p?.authRequiredGate ?? DEFAULT_POLICY.authRequiredGate,
14
+ };
15
+ }
16
+ function countBySeverity(gaps) {
17
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
18
+ for (const g of gaps)
19
+ counts[g.severity]++;
20
+ return counts;
21
+ }
22
+ function deriveCoverageStatus(result) {
23
+ const g = result.gapAnalysis;
24
+ if (g.mode === 'auth-required' || g.coverageWarning === 'auth-required') {
25
+ return 'blocked-by-auth';
26
+ }
27
+ if (g.coverageWarning === 'budget-exceeded' || g.coverageBudgetExceeded) {
28
+ return 'budget-exceeded';
29
+ }
30
+ if (g.coverageWarning === 'navigation-failures') {
31
+ return 'navigation-failures';
32
+ }
33
+ if (g.coverageWarning === 'low-coverage') {
34
+ return 'thin';
35
+ }
36
+ if (g.coveragePagesScanned === 0) {
37
+ return 'unknown';
38
+ }
39
+ return 'ok';
40
+ }
41
+ function buildTopRisks(result, limit) {
42
+ const risks = [];
43
+ const status = deriveCoverageStatus(result);
44
+ if (status === 'blocked-by-auth') {
45
+ risks.push('Auth blocked the scan; protected routes were not exercised.');
46
+ }
47
+ else if (status === 'thin') {
48
+ risks.push('Crawl coverage was below the confidence floor.');
49
+ }
50
+ else if (status === 'budget-exceeded') {
51
+ risks.push('Crawl budget exceeded; coverage is partial.');
52
+ }
53
+ else if (status === 'navigation-failures') {
54
+ risks.push('Navigation errors limited what could be scanned.');
55
+ }
56
+ const sorted = [...result.gaps].sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
57
+ for (const g of sorted) {
58
+ if (risks.length >= limit)
59
+ break;
60
+ risks.push(`[${g.severity}] ${g.category} — ${g.path}`);
61
+ }
62
+ return risks.slice(0, limit);
63
+ }
64
+ function buildRecommendedNextChecks(result, limit) {
65
+ const out = [];
66
+ const status = deriveCoverageStatus(result);
67
+ if (status === 'blocked-by-auth') {
68
+ out.push('Provide auth (form login or storage state) and re-run, or use explore_auth.');
69
+ }
70
+ if (status === 'thin') {
71
+ out.push('Increase crawl budget or supply deeper entry URLs to raise coverage above the floor.');
72
+ }
73
+ if (status === 'budget-exceeded') {
74
+ out.push('Increase maxPagesToScan or narrow scope to bring the crawl under budget.');
75
+ }
76
+ const byCat = new Map();
77
+ for (const g of result.gaps)
78
+ byCat.set(g.category, (byCat.get(g.category) ?? 0) + 1);
79
+ const topCats = [...byCat.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
80
+ for (const [cat, n] of topCats) {
81
+ if (out.length >= limit)
82
+ break;
83
+ out.push(`Add or tighten deterministic coverage for ${cat} (${n} gap(s) this scan).`);
84
+ }
85
+ return out.slice(0, limit);
86
+ }
87
+ function buildHonestyNotes(result) {
88
+ const notes = ['This scan does not guarantee production readiness.'];
89
+ const g = result.gapAnalysis;
90
+ if (g.mode === 'auth-required' || g.coverageWarning === 'auth-required') {
91
+ notes.push('Authenticated surface was not exercised: confidence does not reflect protected routes.');
92
+ }
93
+ if (g.coverageWarning === 'low-coverage') {
94
+ notes.push('Coverage was below the confidence threshold; treat the score as a ceiling.');
95
+ }
96
+ if (g.coverageBudgetExceeded || g.coverageWarning === 'budget-exceeded') {
97
+ notes.push('Crawl budget was exceeded; some routes were not scanned.');
98
+ }
99
+ if (g.coverageWarning === 'navigation-failures') {
100
+ notes.push('Navigation failures reduced effective coverage.');
101
+ }
102
+ if (result.status === 'partial') {
103
+ notes.push('Scan completed only partially; signals are derived from a subset of intended work.');
104
+ }
105
+ if (result.status === 'blocked') {
106
+ notes.push('Scan was blocked before producing a meaningful evaluation.');
107
+ }
108
+ return notes;
109
+ }
110
+ function buildCostSummary(ci) {
111
+ if (!ci)
112
+ return null;
113
+ return {
114
+ maxOutputTokensPerLlmCall: ci.maxOutputTokensPerLlmCall,
115
+ totalInputTokens: ci.usageSummary.totalInputTokens,
116
+ totalOutputTokens: ci.usageSummary.totalOutputTokens,
117
+ budgetWarningCount: ci.budgetWarnings.length,
118
+ dataQuality: ci.usageSummary.dataQuality,
119
+ maturityLevel: ci.deterministicMaturity.level,
120
+ maturityLabel: ci.deterministicMaturity.label,
121
+ };
122
+ }
123
+ function buildDeterministicFollowUps(ci, limit) {
124
+ if (!ci)
125
+ return [];
126
+ return ci.conversionRecommendations.slice(0, limit);
127
+ }
128
+ /**
129
+ * Default gate policy:
130
+ * - fail when: any critical gap, status === 'blocked', releaseConfidence is null,
131
+ * releaseConfidence < failConfidenceThreshold, or mode === 'auth-required'
132
+ * under the default authRequiredGate.
133
+ * - warn when: any high-severity gap, status === 'partial', coverage is thin /
134
+ * budget-exceeded / navigation-failures, or confidence is below
135
+ * passConfidenceThreshold (but at or above failConfidenceThreshold).
136
+ * - pass when: confidence >= passConfidenceThreshold AND no blocking conditions.
137
+ *
138
+ * Honesty rule: an `auth-required` scan never silently `pass`es under defaults.
139
+ */
140
+ function deriveGate(result, policy) {
141
+ const g = result.gapAnalysis;
142
+ const counts = countBySeverity(result.gaps);
143
+ if (counts.critical > 0)
144
+ return 'fail';
145
+ if (result.status === 'blocked')
146
+ return 'fail';
147
+ if (g.mode === 'auth-required' || g.coverageWarning === 'auth-required') {
148
+ return policy.authRequiredGate;
149
+ }
150
+ if (result.releaseConfidence === null)
151
+ return 'fail';
152
+ if (result.releaseConfidence < policy.failConfidenceThreshold)
153
+ return 'fail';
154
+ const hasCoverageIssue = g.coverageWarning === 'low-coverage' ||
155
+ g.coverageWarning === 'budget-exceeded' ||
156
+ g.coverageWarning === 'navigation-failures' ||
157
+ g.coverageBudgetExceeded;
158
+ if (counts.high > 0)
159
+ return 'warn';
160
+ if (result.status === 'partial')
161
+ return 'warn';
162
+ if (hasCoverageIssue)
163
+ return 'warn';
164
+ if (result.releaseConfidence < policy.passConfidenceThreshold)
165
+ return 'warn';
166
+ return 'pass';
167
+ }
168
+ /**
169
+ * Convert an `AnalyzeResult` into a small agent-facing summary.
170
+ *
171
+ * Pure function. No I/O. CLI / MCP wiring belongs to QLIB-001-C03 and -C04.
172
+ */
173
+ export function toAgentSummary(result, policy) {
174
+ const resolved = resolvePolicy(policy);
175
+ const limit = resolved.maxListLength;
176
+ return {
177
+ schemaVersion: 1,
178
+ gate: deriveGate(result, resolved),
179
+ releaseConfidence: result.releaseConfidence,
180
+ coverageStatus: deriveCoverageStatus(result),
181
+ topRisks: buildTopRisks(result, limit),
182
+ recommendedNextChecks: buildRecommendedNextChecks(result, limit),
183
+ honestyNotes: buildHonestyNotes(result),
184
+ costSummary: buildCostSummary(result.gapAnalysis.costIntelligence),
185
+ deterministicFollowUps: buildDeterministicFollowUps(result.gapAnalysis.costIntelligence, limit),
186
+ };
187
+ }
package/dist/cli/index.js CHANGED
@@ -8,6 +8,7 @@ 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 { toAgentSummary } from '../agent-summary.js';
11
12
  import { detectAuth } from '../tools/auth/detect.js';
12
13
  import { exploreAuth } from '../tools/auth/explore.js';
13
14
  import { assertExactlyOneCredentialSource, parseCredentialsJsonString, resolveAuthLoginConfig, } from './auth-login-resolve.js';
@@ -80,15 +81,22 @@ function mergeAuthFromCli(config, options) {
80
81
  return config;
81
82
  }
82
83
  async function runAnalyze(options) {
84
+ if (options.ephemeral && options.agentSummary) {
85
+ throw new Error('Use either --agent-summary or --ephemeral, not both — pick one stdout output mode.');
86
+ }
83
87
  const validatedUrl = AnalyzeUrlSchema.parse(options.url);
84
88
  const mode = options.repo ? 'url-repo' : 'url-only';
85
89
  const baseConfig = await loadConfigFile(options.configFile ?? 'qulib.config.ts');
86
90
  const config = mergeAuthFromCli(baseConfig, options);
87
91
  const ephemeral = options.ephemeral ?? false;
88
- const writeArtifacts = !ephemeral;
92
+ const agentSummary = options.agentSummary ?? false;
93
+ const writeArtifacts = !ephemeral && !agentSummary;
89
94
  if (ephemeral) {
90
95
  console.error('[qulib] Ephemeral mode: no disk writes; full result JSON on stdout');
91
96
  }
97
+ else if (agentSummary) {
98
+ console.error('[qulib] Agent-summary mode: no disk writes; agent gate JSON on stdout');
99
+ }
92
100
  else {
93
101
  console.log('[qulib] Detected mode:', mode);
94
102
  console.log('[qulib] Active config:', redactConfigForLog(config));
@@ -100,6 +108,10 @@ async function runAnalyze(options) {
100
108
  writeArtifacts,
101
109
  skipAuthDetection: options.skipAuthDetection,
102
110
  });
111
+ if (agentSummary) {
112
+ console.log(JSON.stringify(toAgentSummary(result), null, 2));
113
+ return;
114
+ }
103
115
  if (ephemeral) {
104
116
  console.log(JSON.stringify({
105
117
  status: result.status,
@@ -158,6 +170,7 @@ program
158
170
  .option('--config <file>', 'Path to config file (relative to cwd)', 'qulib.config.ts')
159
171
  .option('--adapter <type>', 'Override default test adapter (playwright, cypress-e2e, cypress-component, api)', 'playwright')
160
172
  .option('--ephemeral', 'Do not write to disk — return full report as JSON on stdout (use for MCP/CI)', false)
173
+ .option('--agent-summary', 'Do not write to disk — return the compact agent-summary gate JSON on stdout (pass/warn/fail with honesty notes). Mutually exclusive with --ephemeral.', false)
161
174
  .option('--skip-auth-detection', 'Crawl the public surface even if auth is detected (useful for sites with sign-in CTAs on public pages)', false)
162
175
  .option('--auth-storage-state <path>', 'Path to a storage state JSON file (use after `qulib auth init`)')
163
176
  .option('--auth-form-login', 'Use form-login; requires --login-url, credentials, and selectors', false)
@@ -181,6 +194,7 @@ program
181
194
  repo: options.repo,
182
195
  configFile: options.config,
183
196
  ephemeral: options.ephemeral,
197
+ agentSummary: options.agentSummary,
184
198
  skipAuthDetection: Boolean(options.skipAuthDetection),
185
199
  authStorageState: options.authStorageState,
186
200
  authFormLogin,
@@ -225,9 +239,9 @@ providersCmd
225
239
  providersCmd
226
240
  .command('add')
227
241
  .description('Register a custom provider pattern (case-insensitive regex source)')
228
- .requiredOption('--id <id>', 'Stable id (kebab-case), e.g. scholastic-sync')
242
+ .requiredOption('--id <id>', 'Stable id (kebab-case), e.g. nq-login')
229
243
  .requiredOption('--label <label>', 'Human-readable label')
230
- .requiredOption('--pattern <regex>', 'Regex source, e.g. scholastic sync')
244
+ .requiredOption('--pattern <regex>', 'Regex source, e.g. nq login')
231
245
  .action(async (opts) => {
232
246
  try {
233
247
  new RegExp(opts.pattern, 'i');
@@ -289,7 +303,7 @@ authCmd
289
303
  .command('login')
290
304
  .description('Detect form-login on the URL, fill credentials, and save the storage state automatically (uses selectors from detect-auth)')
291
305
  .requiredOption('--base-url <url>', 'The base URL of the app to log into')
292
- .option('--auth-path <id>', 'Specific authOption id to use (e.g. "scholastic-sync") when multiple form-login paths exist')
306
+ .option('--auth-path <id>', 'Specific authOption id to use (e.g. "nq-login") when multiple form-login paths exist')
293
307
  .option('--credentials <json>', 'JSON object mapping field name → value, e.g. \'{"username":"a","password":"b","hidden.datasource":"NYC"}\'')
294
308
  .option('--credentials-file <path>', 'Path to a JSON file with the credentials object (keeps secrets out of shell history)')
295
309
  .option('--out <path>', 'Output file path for the storage state JSON', './qulib-storage-state.json')
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { analyzeApp } from './analyze.js';
2
+ export { toAgentSummary } from './agent-summary.js';
3
+ export type { AgentSummary, AgentSummaryPolicy, AgentGate, CoverageStatus, AgentSummaryCostSummary, } from './agent-summary.js';
2
4
  export { detectAuth, validateStorageState, evaluateStorageStateValidity, preflightStorageStateFile, waitForReturnToOrigin, } from './tools/auth/detect.js';
3
5
  export type { StorageStateInvalidReason, StorageStateValidationResult, } from './tools/auth/detect.js';
4
6
  export { exploreAuth } from './tools/auth/explore.js';
@@ -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,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"}
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,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,SAAS,EACT,cAAc,EACd,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,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,4 +1,5 @@
1
1
  export { analyzeApp } from './analyze.js';
2
+ export { toAgentSummary } from './agent-summary.js';
2
3
  export { detectAuth, validateStorageState, evaluateStorageStateValidity, preflightStorageStateFile, waitForReturnToOrigin, } from './tools/auth/detect.js';
3
4
  export { exploreAuth } from './tools/auth/explore.js';
4
5
  export { addUserProvider, removeUserProvider, listUserProviders } from './tools/auth/custom-providers.js';
@@ -7,6 +7,14 @@ export interface StorageStateValidationResult {
7
7
  reasonCode: StorageStateInvalidReason | 'ok';
8
8
  reason: string;
9
9
  }
10
+ export declare function pickCredentialFieldName(attrs: {
11
+ name?: string | null;
12
+ id?: string | null;
13
+ autocomplete?: string | null;
14
+ type?: string | null;
15
+ placeholder?: string | null;
16
+ ariaLabel?: string | null;
17
+ }): string;
10
18
  export declare function evaluateStorageStateValidity(signals: {
11
19
  expectedOrigin: string;
12
20
  finalUrl: string;
@@ -1 +1 @@
1
- {"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/detect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAY,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAIzE,MAAM,MAAM,yBAAyB,GACjC,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,cAAc,GACd,yBAAyB,GACzB,iBAAiB,GACjB,SAAS,CAAC;AAEd,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,yBAAyB,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB;AAuRD,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACpD,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,OAAO,CAAC;CAC9B,GAAG,4BAA4B,CA6B/B;AAOD,wBAAsB,yBAAyB,CAC7C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC,CAgD9C;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,EACf,SAAS,SAAQ,GAChB,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAelD;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,SAAS,SAAQ,GAChB,OAAO,CAAC,4BAA4B,CAAC,CAyDvC;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAqJvB"}
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;AA4FD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,MAAM,CAgBT;AAiMD,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"}
@@ -83,21 +83,45 @@ async function resolveVisibleFieldLabel(page, el) {
83
83
  const typ = (await el.getAttribute('type'))?.trim();
84
84
  return typ && typ !== 'select' ? typ : 'text';
85
85
  }
86
- async function deriveCredentialFieldName(el) {
87
- const name = (await el.getAttribute('name'))?.trim();
86
+ function looksLikeExampleValue(s) {
87
+ return /@|:\/\//.test(s);
88
+ }
89
+ export function pickCredentialFieldName(attrs) {
90
+ const name = attrs.name?.trim();
88
91
  if (name)
89
92
  return name;
90
- const placeholder = (await el.getAttribute('placeholder'))?.trim();
91
- if (placeholder)
92
- return slugify(placeholder);
93
- const aria = (await el.getAttribute('aria-label'))?.trim();
94
- if (aria)
95
- return slugify(aria);
96
- const id = (await el.getAttribute('id'))?.trim();
93
+ const id = attrs.id?.trim();
97
94
  if (id)
98
95
  return slugify(id);
96
+ const autocomplete = attrs.autocomplete?.trim().toLowerCase();
97
+ if (autocomplete === 'username' || autocomplete === 'email')
98
+ return autocomplete;
99
+ if (autocomplete === 'current-password' || autocomplete === 'new-password')
100
+ return 'password';
101
+ const type = attrs.type?.trim().toLowerCase();
102
+ if (type === 'email')
103
+ return 'email';
104
+ if (type === 'password')
105
+ return 'password';
106
+ const placeholder = attrs.placeholder?.trim();
107
+ if (placeholder && !looksLikeExampleValue(placeholder))
108
+ return slugify(placeholder);
109
+ const aria = attrs.ariaLabel?.trim();
110
+ if (aria && !looksLikeExampleValue(aria))
111
+ return slugify(aria);
99
112
  return 'field';
100
113
  }
114
+ async function deriveCredentialFieldName(el) {
115
+ const [name, id, autocomplete, type, placeholder, ariaLabel] = await Promise.all([
116
+ el.getAttribute('name'),
117
+ el.getAttribute('id'),
118
+ el.getAttribute('autocomplete'),
119
+ el.getAttribute('type'),
120
+ el.getAttribute('placeholder'),
121
+ el.getAttribute('aria-label'),
122
+ ]);
123
+ return pickCredentialFieldName({ name, id, autocomplete, type, placeholder, ariaLabel });
124
+ }
101
125
  async function buildCredentialFieldsFromVisibleForm(page) {
102
126
  const fields = [];
103
127
  const seen = new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qulib/core",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Qulib — analyze deployed web apps for honest quality gaps (CLI + programmatic API)",
5
5
  "license": "MIT",
6
6
  "author": "Tapesh Nagarwal",
@@ -48,7 +48,7 @@
48
48
  "analyze": "tsx src/cli/index.ts analyze",
49
49
  "clean": "tsx src/cli/index.ts clean",
50
50
  "build": "tsc",
51
- "test": "node --import tsx/esm --test src/llm/__tests__/cost-intelligence.test.ts src/llm/__tests__/context-builder.test.ts src/tools/scoring/__tests__/gaps.test.ts src/tools/auth/__tests__/gaps.test.ts src/tools/auth/__tests__/detect.test.ts src/tools/scoring/__tests__/automation-maturity.test.ts src/harness/__tests__/state-manager.test.ts src/telemetry/__tests__/redact-url.test.ts src/cli/__tests__/auth-login.test.ts src/cli/__tests__/cli-version.test.ts src/__tests__/analyze.storage-state-invalid.test.ts src/__tests__/analyze.fixtures.test.ts",
51
+ "test": "node --import tsx/esm --test src/llm/__tests__/cost-intelligence.test.ts src/llm/__tests__/context-builder.test.ts src/tools/scoring/__tests__/gaps.test.ts src/tools/auth/__tests__/gaps.test.ts src/tools/auth/__tests__/detect.test.ts src/tools/scoring/__tests__/automation-maturity.test.ts src/harness/__tests__/state-manager.test.ts src/telemetry/__tests__/redact-url.test.ts src/cli/__tests__/auth-login.test.ts src/cli/__tests__/cli-version.test.ts src/__tests__/agent-summary.test.ts src/__tests__/cli-agent-summary.test.ts src/__tests__/analyze.storage-state-invalid.test.ts src/__tests__/analyze.fixtures.test.ts",
52
52
  "test:integration": "node --import tsx/esm --test src/__tests__/analyze.integration.test.ts",
53
53
  "smoke": "tsx src/cli/index.ts analyze --url https://example.com --ephemeral",
54
54
  "cost-doctor": "tsx src/cli/index.ts cost doctor"
@@ -1,4 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,35 +0,0 @@
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
- }
@@ -1,9 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,52 +0,0 @@
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
- }
@@ -1,23 +0,0 @@
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=detector.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/detector.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"}