@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.
- package/README.md +31 -3
- package/dist/analyze.d.ts +16 -4
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +98 -38
- package/dist/cli/cost-doctor.d.ts +2 -0
- package/dist/cli/cost-doctor.d.ts.map +1 -0
- package/dist/cli/cost-doctor.js +72 -0
- package/dist/cli/index.js +14 -0
- package/dist/harness/progress-log.d.ts +7 -0
- package/dist/harness/progress-log.d.ts.map +1 -0
- package/dist/harness/progress-log.js +1 -0
- package/dist/harness/run-options.d.ts +2 -0
- package/dist/harness/run-options.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/llm/content-hash.d.ts +2 -0
- package/dist/llm/content-hash.d.ts.map +1 -0
- package/dist/llm/content-hash.js +4 -0
- package/dist/llm/context-builder.js +1 -1
- package/dist/llm/cost-intelligence.d.ts +29 -0
- package/dist/llm/cost-intelligence.d.ts.map +1 -0
- package/dist/llm/cost-intelligence.js +153 -0
- package/dist/llm/provider.d.ts +11 -1
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/llm/provider.js +43 -4
- package/dist/phases/act.d.ts.map +1 -1
- package/dist/phases/act.js +4 -1
- package/dist/phases/observe.js +1 -1
- package/dist/phases/think-finalize.d.ts +6 -0
- package/dist/phases/think-finalize.d.ts.map +1 -0
- package/dist/phases/think-finalize.js +164 -0
- package/dist/phases/think.d.ts +2 -0
- package/dist/phases/think.d.ts.map +1 -1
- package/dist/phases/think.js +16 -65
- package/dist/reporters/markdown-reporter.d.ts.map +1 -1
- package/dist/reporters/markdown-reporter.js +23 -3
- package/dist/schemas/config.schema.d.ts +7 -0
- package/dist/schemas/config.schema.d.ts.map +1 -1
- package/dist/schemas/config.schema.js +18 -1
- package/dist/schemas/cost-intelligence.schema.d.ts +229 -0
- package/dist/schemas/cost-intelligence.schema.d.ts.map +1 -0
- package/dist/schemas/cost-intelligence.schema.js +41 -0
- package/dist/schemas/decision-log.schema.d.ts +2 -2
- package/dist/schemas/gap-analysis.schema.d.ts +270 -31
- package/dist/schemas/gap-analysis.schema.d.ts.map +1 -1
- package/dist/schemas/gap-analysis.schema.js +7 -3
- package/dist/schemas/index.d.ts +3 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +3 -1
- package/dist/schemas/public-surface.schema.d.ts +268 -0
- package/dist/schemas/public-surface.schema.d.ts.map +1 -0
- package/dist/schemas/public-surface.schema.js +15 -0
- package/dist/tools/auth-block-gap.d.ts +3 -0
- package/dist/tools/auth-block-gap.d.ts.map +1 -0
- package/dist/tools/auth-block-gap.js +19 -0
- package/dist/tools/auth-detector.d.ts +2 -1
- package/dist/tools/auth-detector.d.ts.map +1 -1
- package/dist/tools/auth-detector.js +28 -3
- package/dist/tools/auth-explorer.d.ts +2 -1
- package/dist/tools/auth-explorer.d.ts.map +1 -1
- package/dist/tools/auth-explorer.js +30 -8
- 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 +154 -0
- package/dist/tools/cypress-explorer.d.ts +2 -1
- package/dist/tools/cypress-explorer.d.ts.map +1 -1
- package/dist/tools/cypress-explorer.js +1 -1
- package/dist/tools/explorer.interface.d.ts +2 -1
- package/dist/tools/explorer.interface.d.ts.map +1 -1
- package/dist/tools/gap-engine.d.ts +3 -1
- package/dist/tools/gap-engine.d.ts.map +1 -1
- package/dist/tools/gap-engine.js +39 -12
- package/dist/tools/playwright-explorer.d.ts +2 -1
- package/dist/tools/playwright-explorer.d.ts.map +1 -1
- package/dist/tools/playwright-explorer.js +21 -3
- package/dist/tools/public-surface.d.ts +5 -0
- package/dist/tools/public-surface.d.ts.map +1 -0
- package/dist/tools/public-surface.js +13 -0
- 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 @@
|
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
|
227
|
+
let providerMatch = null;
|
|
217
228
|
for (const p of allProviders) {
|
|
218
|
-
|
|
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
|
-
|
|
239
|
+
providerMatch = { p, gate: textLooksLikeOAuthIdpButton(text) || loginish };
|
|
225
240
|
break;
|
|
226
241
|
}
|
|
227
|
-
if (
|
|
228
|
-
const { p, gate } =
|
|
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"}
|