@qulib/core 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +31 -3
  2. package/dist/analyze.d.ts +16 -4
  3. package/dist/analyze.d.ts.map +1 -1
  4. package/dist/analyze.js +98 -38
  5. package/dist/cli/cost-doctor.d.ts +2 -0
  6. package/dist/cli/cost-doctor.d.ts.map +1 -0
  7. package/dist/cli/cost-doctor.js +72 -0
  8. package/dist/cli/index.js +14 -0
  9. package/dist/harness/progress-log.d.ts +7 -0
  10. package/dist/harness/progress-log.d.ts.map +1 -0
  11. package/dist/harness/progress-log.js +1 -0
  12. package/dist/harness/run-options.d.ts +2 -0
  13. package/dist/harness/run-options.d.ts.map +1 -1
  14. package/dist/index.d.ts +4 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -0
  17. package/dist/llm/content-hash.d.ts +2 -0
  18. package/dist/llm/content-hash.d.ts.map +1 -0
  19. package/dist/llm/content-hash.js +4 -0
  20. package/dist/llm/context-builder.js +1 -1
  21. package/dist/llm/cost-intelligence.d.ts +29 -0
  22. package/dist/llm/cost-intelligence.d.ts.map +1 -0
  23. package/dist/llm/cost-intelligence.js +153 -0
  24. package/dist/llm/provider.d.ts +11 -1
  25. package/dist/llm/provider.d.ts.map +1 -1
  26. package/dist/llm/provider.js +43 -4
  27. package/dist/phases/act.d.ts.map +1 -1
  28. package/dist/phases/act.js +4 -1
  29. package/dist/phases/observe.js +1 -1
  30. package/dist/phases/think-finalize.d.ts +6 -0
  31. package/dist/phases/think-finalize.d.ts.map +1 -0
  32. package/dist/phases/think-finalize.js +164 -0
  33. package/dist/phases/think.d.ts +2 -0
  34. package/dist/phases/think.d.ts.map +1 -1
  35. package/dist/phases/think.js +16 -65
  36. package/dist/reporters/markdown-reporter.d.ts.map +1 -1
  37. package/dist/reporters/markdown-reporter.js +23 -3
  38. package/dist/schemas/config.schema.d.ts +7 -0
  39. package/dist/schemas/config.schema.d.ts.map +1 -1
  40. package/dist/schemas/config.schema.js +18 -1
  41. package/dist/schemas/cost-intelligence.schema.d.ts +229 -0
  42. package/dist/schemas/cost-intelligence.schema.d.ts.map +1 -0
  43. package/dist/schemas/cost-intelligence.schema.js +41 -0
  44. package/dist/schemas/decision-log.schema.d.ts +2 -2
  45. package/dist/schemas/gap-analysis.schema.d.ts +270 -31
  46. package/dist/schemas/gap-analysis.schema.d.ts.map +1 -1
  47. package/dist/schemas/gap-analysis.schema.js +7 -3
  48. package/dist/schemas/index.d.ts +3 -1
  49. package/dist/schemas/index.d.ts.map +1 -1
  50. package/dist/schemas/index.js +3 -1
  51. package/dist/schemas/public-surface.schema.d.ts +268 -0
  52. package/dist/schemas/public-surface.schema.d.ts.map +1 -0
  53. package/dist/schemas/public-surface.schema.js +15 -0
  54. package/dist/tools/auth-block-gap.d.ts +3 -0
  55. package/dist/tools/auth-block-gap.d.ts.map +1 -0
  56. package/dist/tools/auth-block-gap.js +19 -0
  57. package/dist/tools/auth-detector.d.ts +2 -1
  58. package/dist/tools/auth-detector.d.ts.map +1 -1
  59. package/dist/tools/auth-detector.js +28 -3
  60. package/dist/tools/auth-explorer.d.ts +2 -1
  61. package/dist/tools/auth-explorer.d.ts.map +1 -1
  62. package/dist/tools/auth-explorer.js +30 -8
  63. package/dist/tools/auth-surface-analyzer.d.ts +4 -0
  64. package/dist/tools/auth-surface-analyzer.d.ts.map +1 -0
  65. package/dist/tools/auth-surface-analyzer.js +154 -0
  66. package/dist/tools/cypress-explorer.d.ts +2 -1
  67. package/dist/tools/cypress-explorer.d.ts.map +1 -1
  68. package/dist/tools/cypress-explorer.js +1 -1
  69. package/dist/tools/explorer.interface.d.ts +2 -1
  70. package/dist/tools/explorer.interface.d.ts.map +1 -1
  71. package/dist/tools/gap-engine.d.ts +3 -1
  72. package/dist/tools/gap-engine.d.ts.map +1 -1
  73. package/dist/tools/gap-engine.js +39 -12
  74. package/dist/tools/playwright-explorer.d.ts +2 -1
  75. package/dist/tools/playwright-explorer.d.ts.map +1 -1
  76. package/dist/tools/playwright-explorer.js +21 -3
  77. package/dist/tools/public-surface.d.ts +5 -0
  78. package/dist/tools/public-surface.d.ts.map +1 -0
  79. package/dist/tools/public-surface.js +13 -0
  80. package/package.json +7 -3
@@ -0,0 +1,268 @@
1
+ import { z } from 'zod';
2
+ export declare const PublicSurfaceViolationSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ impact: z.ZodString;
5
+ helpUrl: z.ZodString;
6
+ nodeCount: z.ZodNumber;
7
+ } & {
8
+ path: z.ZodString;
9
+ }, "strip", z.ZodTypeAny, {
10
+ path: string;
11
+ id: string;
12
+ impact: string;
13
+ helpUrl: string;
14
+ nodeCount: number;
15
+ }, {
16
+ path: string;
17
+ id: string;
18
+ impact: string;
19
+ helpUrl: string;
20
+ nodeCount: number;
21
+ }>;
22
+ export declare const PublicSurfaceBrokenLinkSchema: z.ZodObject<{
23
+ url: z.ZodString;
24
+ status: z.ZodNullable<z.ZodNumber>;
25
+ reason: z.ZodOptional<z.ZodString>;
26
+ } & {
27
+ path: z.ZodString;
28
+ }, "strip", z.ZodTypeAny, {
29
+ status: number | null;
30
+ path: string;
31
+ url: string;
32
+ reason?: string | undefined;
33
+ }, {
34
+ status: number | null;
35
+ path: string;
36
+ url: string;
37
+ reason?: string | undefined;
38
+ }>;
39
+ export declare const PublicSurfaceSchema: z.ZodObject<{
40
+ pages: z.ZodArray<z.ZodObject<{
41
+ path: z.ZodString;
42
+ pageTitle: z.ZodString;
43
+ links: z.ZodArray<z.ZodString, "many">;
44
+ formCount: z.ZodNumber;
45
+ buttonLabels: z.ZodArray<z.ZodString, "many">;
46
+ consoleErrors: z.ZodArray<z.ZodString, "many">;
47
+ brokenLinks: z.ZodArray<z.ZodObject<{
48
+ url: z.ZodString;
49
+ status: z.ZodNullable<z.ZodNumber>;
50
+ reason: z.ZodOptional<z.ZodString>;
51
+ }, "strip", z.ZodTypeAny, {
52
+ status: number | null;
53
+ url: string;
54
+ reason?: string | undefined;
55
+ }, {
56
+ status: number | null;
57
+ url: string;
58
+ reason?: string | undefined;
59
+ }>, "many">;
60
+ a11yViolations: z.ZodArray<z.ZodObject<{
61
+ id: z.ZodString;
62
+ impact: z.ZodString;
63
+ helpUrl: z.ZodString;
64
+ nodeCount: z.ZodNumber;
65
+ }, "strip", z.ZodTypeAny, {
66
+ id: string;
67
+ impact: string;
68
+ helpUrl: string;
69
+ nodeCount: number;
70
+ }, {
71
+ id: string;
72
+ impact: string;
73
+ helpUrl: string;
74
+ nodeCount: number;
75
+ }>, "many">;
76
+ statusCode: z.ZodOptional<z.ZodNumber>;
77
+ }, "strip", z.ZodTypeAny, {
78
+ path: string;
79
+ pageTitle: string;
80
+ links: string[];
81
+ formCount: number;
82
+ buttonLabels: string[];
83
+ consoleErrors: string[];
84
+ brokenLinks: {
85
+ status: number | null;
86
+ url: string;
87
+ reason?: string | undefined;
88
+ }[];
89
+ a11yViolations: {
90
+ id: string;
91
+ impact: string;
92
+ helpUrl: string;
93
+ nodeCount: number;
94
+ }[];
95
+ statusCode?: number | undefined;
96
+ }, {
97
+ path: string;
98
+ pageTitle: string;
99
+ links: string[];
100
+ formCount: number;
101
+ buttonLabels: string[];
102
+ consoleErrors: string[];
103
+ brokenLinks: {
104
+ status: number | null;
105
+ url: string;
106
+ reason?: string | undefined;
107
+ }[];
108
+ a11yViolations: {
109
+ id: string;
110
+ impact: string;
111
+ helpUrl: string;
112
+ nodeCount: number;
113
+ }[];
114
+ statusCode?: number | undefined;
115
+ }>, "many">;
116
+ gaps: z.ZodArray<z.ZodObject<{
117
+ id: z.ZodString;
118
+ path: z.ZodString;
119
+ severity: z.ZodEnum<["critical", "high", "medium", "low"]>;
120
+ reason: z.ZodString;
121
+ category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage"]>;
122
+ description: z.ZodOptional<z.ZodString>;
123
+ recommendation: z.ZodOptional<z.ZodString>;
124
+ }, "strip", z.ZodTypeAny, {
125
+ path: string;
126
+ id: string;
127
+ severity: "high" | "medium" | "low" | "critical";
128
+ reason: string;
129
+ category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage";
130
+ recommendation?: string | undefined;
131
+ description?: string | undefined;
132
+ }, {
133
+ path: string;
134
+ id: string;
135
+ severity: "high" | "medium" | "low" | "critical";
136
+ reason: string;
137
+ category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage";
138
+ recommendation?: string | undefined;
139
+ description?: string | undefined;
140
+ }>, "many">;
141
+ accessibilityViolations: z.ZodArray<z.ZodObject<{
142
+ id: z.ZodString;
143
+ impact: z.ZodString;
144
+ helpUrl: z.ZodString;
145
+ nodeCount: z.ZodNumber;
146
+ } & {
147
+ path: z.ZodString;
148
+ }, "strip", z.ZodTypeAny, {
149
+ path: string;
150
+ id: string;
151
+ impact: string;
152
+ helpUrl: string;
153
+ nodeCount: number;
154
+ }, {
155
+ path: string;
156
+ id: string;
157
+ impact: string;
158
+ helpUrl: string;
159
+ nodeCount: number;
160
+ }>, "many">;
161
+ brokenLinks: z.ZodArray<z.ZodObject<{
162
+ url: z.ZodString;
163
+ status: z.ZodNullable<z.ZodNumber>;
164
+ reason: z.ZodOptional<z.ZodString>;
165
+ } & {
166
+ path: z.ZodString;
167
+ }, "strip", z.ZodTypeAny, {
168
+ status: number | null;
169
+ path: string;
170
+ url: string;
171
+ reason?: string | undefined;
172
+ }, {
173
+ status: number | null;
174
+ path: string;
175
+ url: string;
176
+ reason?: string | undefined;
177
+ }>, "many">;
178
+ }, "strip", z.ZodTypeAny, {
179
+ gaps: {
180
+ path: string;
181
+ id: string;
182
+ severity: "high" | "medium" | "low" | "critical";
183
+ reason: string;
184
+ category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage";
185
+ recommendation?: string | undefined;
186
+ description?: string | undefined;
187
+ }[];
188
+ brokenLinks: {
189
+ status: number | null;
190
+ path: string;
191
+ url: string;
192
+ reason?: string | undefined;
193
+ }[];
194
+ pages: {
195
+ path: string;
196
+ pageTitle: string;
197
+ links: string[];
198
+ formCount: number;
199
+ buttonLabels: string[];
200
+ consoleErrors: string[];
201
+ brokenLinks: {
202
+ status: number | null;
203
+ url: string;
204
+ reason?: string | undefined;
205
+ }[];
206
+ a11yViolations: {
207
+ id: string;
208
+ impact: string;
209
+ helpUrl: string;
210
+ nodeCount: number;
211
+ }[];
212
+ statusCode?: number | undefined;
213
+ }[];
214
+ accessibilityViolations: {
215
+ path: string;
216
+ id: string;
217
+ impact: string;
218
+ helpUrl: string;
219
+ nodeCount: number;
220
+ }[];
221
+ }, {
222
+ gaps: {
223
+ path: string;
224
+ id: string;
225
+ severity: "high" | "medium" | "low" | "critical";
226
+ reason: string;
227
+ category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage";
228
+ recommendation?: string | undefined;
229
+ description?: string | undefined;
230
+ }[];
231
+ brokenLinks: {
232
+ status: number | null;
233
+ path: string;
234
+ url: string;
235
+ reason?: string | undefined;
236
+ }[];
237
+ pages: {
238
+ path: string;
239
+ pageTitle: string;
240
+ links: string[];
241
+ formCount: number;
242
+ buttonLabels: string[];
243
+ consoleErrors: string[];
244
+ brokenLinks: {
245
+ status: number | null;
246
+ url: string;
247
+ reason?: string | undefined;
248
+ }[];
249
+ a11yViolations: {
250
+ id: string;
251
+ impact: string;
252
+ helpUrl: string;
253
+ nodeCount: number;
254
+ }[];
255
+ statusCode?: number | undefined;
256
+ }[];
257
+ accessibilityViolations: {
258
+ path: string;
259
+ id: string;
260
+ impact: string;
261
+ helpUrl: string;
262
+ nodeCount: number;
263
+ }[];
264
+ }>;
265
+ export type PublicSurface = z.infer<typeof PublicSurfaceSchema>;
266
+ export type PublicSurfaceViolation = z.infer<typeof PublicSurfaceViolationSchema>;
267
+ export type PublicSurfaceBrokenLink = z.infer<typeof PublicSurfaceBrokenLinkSchema>;
268
+ //# sourceMappingURL=public-surface.schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-surface.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/public-surface.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;EAEvC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;EAExC,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAK9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAClF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ import { GapSchema } from './gap-analysis.schema.js';
3
+ import { A11yViolationSchema, BrokenLinkSchema, RouteSchema } from './route-inventory.schema.js';
4
+ export const PublicSurfaceViolationSchema = A11yViolationSchema.extend({
5
+ path: z.string(),
6
+ });
7
+ export const PublicSurfaceBrokenLinkSchema = BrokenLinkSchema.extend({
8
+ path: z.string(),
9
+ });
10
+ export const PublicSurfaceSchema = z.object({
11
+ pages: z.array(RouteSchema),
12
+ gaps: z.array(GapSchema),
13
+ accessibilityViolations: z.array(PublicSurfaceViolationSchema),
14
+ brokenLinks: z.array(PublicSurfaceBrokenLinkSchema),
15
+ });
@@ -0,0 +1,3 @@
1
+ import type { Gap } from '../schemas/gap-analysis.schema.js';
2
+ export declare function buildAuthBlockGap(url: string): Gap;
3
+ //# sourceMappingURL=auth-block-gap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-block-gap.d.ts","sourceRoot":"","sources":["../../src/tools/auth-block-gap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,mCAAmC,CAAC;AAE7D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAiBlD"}
@@ -0,0 +1,19 @@
1
+ export function buildAuthBlockGap(url) {
2
+ const host = (() => {
3
+ try {
4
+ return new URL(url).hostname;
5
+ }
6
+ catch {
7
+ return url;
8
+ }
9
+ })();
10
+ return {
11
+ id: 'auth-block',
12
+ path: '/',
13
+ severity: 'critical',
14
+ category: 'coverage',
15
+ reason: `Scan blocked by authentication. No authenticated pages were evaluated for ${host}.`,
16
+ description: 'Scan blocked by authentication. 0 authenticated pages were evaluated.',
17
+ recommendation: `Run \`qulib auth init --base-url ${url}\` to capture a storage state, then re-run with --auth storage-state.`,
18
+ };
19
+ }
@@ -1,3 +1,4 @@
1
1
  import type { DetectedAuth } from '../schemas/config.schema.js';
2
- export declare function detectAuth(url: string, timeoutMs?: number): Promise<DetectedAuth>;
2
+ import type { AnalyzeProgressSink } from '../harness/progress-log.js';
3
+ export declare function detectAuth(url: string, timeoutMs?: number, progress?: AnalyzeProgressSink): Promise<DetectedAuth>;
3
4
  //# sourceMappingURL=auth-detector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-detector.d.ts","sourceRoot":"","sources":["../../src/tools/auth-detector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AA4DhE,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,CAkGtF"}
1
+ {"version":3,"file":"auth-detector.d.ts","sourceRoot":"","sources":["../../src/tools/auth-detector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAgEtE,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,YAAY,CAAC,CA8HvB"}
@@ -50,20 +50,31 @@ async function firstTextInputNameForLogin(page) {
50
50
  }
51
51
  return null;
52
52
  }
53
- export async function detectAuth(url, timeoutMs = 15000) {
53
+ function debugAuth() {
54
+ return process.env.QULIB_DEBUG === '1';
55
+ }
56
+ export async function detectAuth(url, timeoutMs = 15000, progress) {
54
57
  const browser = await launchBrowser();
55
58
  try {
56
59
  const context = await browser.newContext();
57
60
  const page = await context.newPage();
61
+ progress?.info(`detect_auth URL=${url}`);
58
62
  await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
59
63
  await waitNetworkIdleBestEffort(page);
64
+ if (debugAuth()) {
65
+ const html = await page.content();
66
+ progress?.debug(`detect_auth HTML byteLength=${Buffer.byteLength(html, 'utf8')}`);
67
+ }
60
68
  let loginUrl = url;
61
69
  const looksLikeLoginPage = /login|sign[- ]?in|auth/i.test(page.url()) ||
62
70
  (await page.locator('input[type="password"]').count()) > 0;
63
71
  if (!looksLikeLoginPage) {
64
72
  const loginLink = page.locator('a').filter({ hasText: /^(log ?in|sign ?in|sign in)$/i }).first();
65
- if ((await loginLink.count()) > 0) {
73
+ const loginLinkCount = await loginLink.count();
74
+ progress?.debug(`detect_auth selector loginLink count=${loginLinkCount}`);
75
+ if (loginLinkCount > 0) {
66
76
  const href = await loginLink.getAttribute('href');
77
+ progress?.debug(`detect_auth selector loginLink href matched=${Boolean(href)}`);
67
78
  if (href) {
68
79
  loginUrl = new URL(href, url).toString();
69
80
  await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
@@ -73,16 +84,24 @@ export async function detectAuth(url, timeoutMs = 15000) {
73
84
  }
74
85
  const passwordInputs = page.locator('input[type="password"]');
75
86
  const passwordCount = await passwordInputs.count();
87
+ progress?.debug(`detect_auth selector input[type=password] count=${passwordCount}`);
76
88
  const hasFormLogin = passwordCount > 0;
77
89
  const oauthButtons = [];
78
90
  const buttonTexts = await page.locator('button, a').allInnerTexts();
79
91
  for (const text of buttonTexts) {
80
92
  const trimmed = text.trim();
81
93
  if (!textLooksLikeOAuthIdpButton(trimmed)) {
94
+ if (debugAuth() && trimmed.length > 0 && trimmed.length <= 120) {
95
+ progress?.debug(`detect_auth oauth text skipped (not Idp-shaped) sample="${trimmed.slice(0, 80)}"`);
96
+ }
82
97
  continue;
83
98
  }
84
99
  for (const { provider, patterns } of OAUTH_PROVIDERS) {
85
- if (patterns.some((p) => p.test(trimmed))) {
100
+ const matched = patterns.some((p) => p.test(trimmed));
101
+ if (debugAuth()) {
102
+ progress?.debug(`detect_auth oauth pattern try provider=${provider} matched=${matched}`);
103
+ }
104
+ if (matched) {
86
105
  if (!oauthButtons.find((b) => b.provider === provider)) {
87
106
  oauthButtons.push({ provider, text: trimmed.slice(0, 100) });
88
107
  }
@@ -114,6 +133,9 @@ export async function detectAuth(url, timeoutMs = 15000) {
114
133
  passwordSelector: passwordName ? `input[name="${passwordName}"]` : null,
115
134
  submitSelector: submitName ? `button[name="${submitName}"]` : 'button[type="submit"]',
116
135
  };
136
+ if (debugAuth()) {
137
+ progress?.debug(`detect_auth resolved selectors username=${observedSelectors.usernameSelector ?? 'null'} password=${observedSelectors.passwordSelector ?? 'null'} submit=${observedSelectors.submitSelector}`);
138
+ }
117
139
  recommendation = `Form login detected. Configure auth with type="form-login", credentials, and the selectors above. Test selectors in your browser dev tools to confirm.`;
118
140
  }
119
141
  else if (hasMagicLink) {
@@ -128,6 +150,9 @@ export async function detectAuth(url, timeoutMs = 15000) {
128
150
  type = 'none';
129
151
  recommendation = `No authentication required for the entry URL. Qulib can scan anonymously.`;
130
152
  }
153
+ const providerList = oauthButtons.length > 0 ? oauthButtons.map((b) => b.provider).join(', ') : provider ?? 'none';
154
+ const automatable = type === 'form-login';
155
+ progress?.info(`Auth detected: ${type} (${providerList}) automatable=${automatable}`);
131
156
  return {
132
157
  hasAuth: type !== 'none',
133
158
  type,
@@ -1,3 +1,4 @@
1
1
  import { type AuthExploration } from '../schemas/config.schema.js';
2
- export declare function exploreAuth(url: string, timeoutMs?: number): Promise<AuthExploration>;
2
+ import type { AnalyzeProgressSink } from '../harness/progress-log.js';
3
+ export declare function exploreAuth(url: string, timeoutMs?: number, progress?: AnalyzeProgressSink): Promise<AuthExploration>;
3
4
  //# sourceMappingURL=auth-explorer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-explorer.d.ts","sourceRoot":"","sources":["../../src/tools/auth-explorer.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,6BAA6B,CAAC;AA6MrC,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,eAAe,CAAC,CA2J1F"}
1
+ {"version":3,"file":"auth-explorer.d.ts","sourceRoot":"","sources":["../../src/tools/auth-explorer.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAiNtE,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAgL1B"}
@@ -36,6 +36,9 @@ function slugifyLabel(text) {
36
36
  function onLoginishPage(url) {
37
37
  return /login|sign[- ]?in|auth|sso|oauth/i.test(new URL(url).pathname + new URL(url).hostname);
38
38
  }
39
+ function debugExplore() {
40
+ return process.env.QULIB_DEBUG === '1';
41
+ }
39
42
  function isHeuristicUnknownSso(text, loginish) {
40
43
  const t = text.trim();
41
44
  if (!loginish || t.length < 3 || t.length > 80) {
@@ -182,18 +185,26 @@ async function buildFormPaths(page) {
182
185
  },
183
186
  ];
184
187
  }
185
- export async function exploreAuth(url, timeoutMs = 20000) {
188
+ export async function exploreAuth(url, timeoutMs = 20000, progress) {
186
189
  const browser = await launchBrowser();
187
190
  try {
188
191
  const context = await browser.newContext();
189
192
  const page = await context.newPage();
193
+ progress?.info(`explore_auth URL=${url}`);
190
194
  await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
191
195
  await waitNetworkIdleBestEffort(page);
196
+ if (debugExplore()) {
197
+ const html = await page.content();
198
+ progress?.debug(`explore_auth HTML byteLength=${Buffer.byteLength(html, 'utf8')}`);
199
+ }
192
200
  const loginishAfterFirst = /login|sign[- ]?in|auth/i.test(page.url()) || (await page.locator('input[type="password"]').count()) > 0;
193
201
  if (!loginishAfterFirst) {
194
202
  const loginLink = page.locator('a').filter({ hasText: /^(log ?in|sign ?in|sign in)$/i }).first();
195
- if ((await loginLink.count()) > 0) {
203
+ const cnt = await loginLink.count();
204
+ progress?.debug(`explore_auth selector loginLink count=${cnt}`);
205
+ if (cnt > 0) {
196
206
  const href = await loginLink.getAttribute('href');
207
+ progress?.debug(`explore_auth selector loginLink href matched=${Boolean(href)}`);
197
208
  if (href) {
198
209
  const next = new URL(href, url).toString();
199
210
  await page.goto(next, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
@@ -213,19 +224,23 @@ export async function exploreAuth(url, timeoutMs = 20000) {
213
224
  if (!text) {
214
225
  continue;
215
226
  }
216
- let matched = null;
227
+ let providerMatch = null;
217
228
  for (const p of allProviders) {
218
- if (!matchProvider(text, p)) {
229
+ const hit = matchProvider(text, p);
230
+ if (debugExplore()) {
231
+ progress?.debug(`explore_auth provider try id=${p.id} matched=${hit}`);
232
+ }
233
+ if (!hit) {
219
234
  continue;
220
235
  }
221
236
  if (p.source === 'built-in' && !(textLooksLikeOAuthIdpButton(text) || loginish)) {
222
237
  continue;
223
238
  }
224
- matched = { p, gate: textLooksLikeOAuthIdpButton(text) || loginish };
239
+ providerMatch = { p, gate: textLooksLikeOAuthIdpButton(text) || loginish };
225
240
  break;
226
241
  }
227
- if (matched) {
228
- const { p, gate } = matched;
242
+ if (providerMatch) {
243
+ const { p, gate } = providerMatch;
229
244
  const id = `oauth:${p.id}`;
230
245
  if (consumed.has(id)) {
231
246
  continue;
@@ -241,6 +256,7 @@ export async function exploreAuth(url, timeoutMs = 20000) {
241
256
  confidence: oauthConfidence(p.source, loginish || gate),
242
257
  requirements: storageRequirement(),
243
258
  });
259
+ progress?.info(`explore_auth path id=${id} type=oauth provider=${p.id} automatable=false`);
244
260
  continue;
245
261
  }
246
262
  if (isHeuristicUnknownSso(text, loginish)) {
@@ -260,6 +276,7 @@ export async function exploreAuth(url, timeoutMs = 20000) {
260
276
  confidence: 'low',
261
277
  requirements: storageRequirement(),
262
278
  });
279
+ progress?.info(`explore_auth path id=${id} type=oauth-unknown automatable=false`);
263
280
  const safePattern = text.slice(0, 48).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
264
281
  unrecognizedButtons.push({
265
282
  label: text.slice(0, 100),
@@ -282,8 +299,13 @@ export async function exploreAuth(url, timeoutMs = 20000) {
282
299
  instruction: 'Magic-link flows need a human in the loop. Use `qulib auth init --base-url <app-url>` and complete email or provider steps in the opened browser, then reuse the saved storage state for scans.',
283
300
  },
284
301
  });
302
+ progress?.info('explore_auth path id=magic-link type=magic-link automatable=false');
303
+ }
304
+ const formPaths = await buildFormPaths(page);
305
+ for (const fp of formPaths) {
306
+ authPaths.push(fp);
307
+ progress?.info(`explore_auth path id=${fp.id} type=${fp.type} automatable=${fp.automatable}`);
285
308
  }
286
- authPaths.push(...(await buildFormPaths(page)));
287
309
  const authRequired = authPaths.length > 0;
288
310
  let authScope = 'none';
289
311
  if (authRequired) {
@@ -0,0 +1,4 @@
1
+ import type { DetectedAuth } from '../schemas/config.schema.js';
2
+ import type { Gap } from '../schemas/gap-analysis.schema.js';
3
+ export declare function analyzeAuthSurfaceGaps(url: string, detection: DetectedAuth, timeoutMs: number): Promise<Gap[]>;
4
+ //# sourceMappingURL=auth-surface-analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-surface-analyzer.d.ts","sourceRoot":"","sources":["../../src/tools/auth-surface-analyzer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,mCAAmC,CAAC;AAW7D,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,YAAY,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,EAAE,CAAC,CAuJhB"}