@qulib/core 0.2.0 → 0.2.2
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 +24 -0
- package/dist/cli/index.js +47 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/schemas/config.schema.d.ts +357 -0
- package/dist/schemas/config.schema.d.ts.map +1 -1
- package/dist/schemas/config.schema.js +37 -0
- package/dist/schemas/gap-analysis.schema.d.ts +18 -18
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/repo-analysis.schema.d.ts +6 -6
- package/dist/tools/auth-detector.d.ts.map +1 -1
- package/dist/tools/auth-detector.js +12 -2
- package/dist/tools/auth-explorer.d.ts +3 -0
- package/dist/tools/auth-explorer.d.ts.map +1 -0
- package/dist/tools/auth-explorer.js +324 -0
- package/dist/tools/browser.d.ts +3 -0
- package/dist/tools/browser.d.ts.map +1 -0
- package/dist/tools/browser.js +13 -0
- package/dist/tools/oauth-providers.d.ts +7 -0
- package/dist/tools/oauth-providers.d.ts.map +1 -0
- package/dist/tools/oauth-providers.js +21 -0
- package/dist/tools/playwright-explorer.d.ts.map +1 -1
- package/dist/tools/playwright-explorer.js +2 -2
- package/dist/tools/user-providers.d.ts +15 -0
- package/dist/tools/user-providers.d.ts.map +1 -0
- package/dist/tools/user-providers.js +62 -0
- package/package.json +1 -1
|
@@ -23,13 +23,13 @@ export declare const FrameworkRecommendationSchema: z.ZodObject<{
|
|
|
23
23
|
reason: z.ZodString;
|
|
24
24
|
confidence: z.ZodEnum<["high", "medium", "low"]>;
|
|
25
25
|
}, "strip", z.ZodTypeAny, {
|
|
26
|
+
confidence: "high" | "medium" | "low";
|
|
26
27
|
reason: string;
|
|
27
28
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
28
|
-
confidence: "high" | "medium" | "low";
|
|
29
29
|
}, {
|
|
30
|
+
confidence: "high" | "medium" | "low";
|
|
30
31
|
reason: string;
|
|
31
32
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
32
|
-
confidence: "high" | "medium" | "low";
|
|
33
33
|
}>;
|
|
34
34
|
export declare const TestStepSchema: z.ZodObject<{
|
|
35
35
|
action: z.ZodEnum<["navigate", "click", "type", "assert-visible", "assert-hidden", "assert-text", "assert-disabled", "assert-count", "wait", "api-call"]>;
|
|
@@ -75,13 +75,13 @@ export declare const NeutralScenarioSchema: z.ZodObject<{
|
|
|
75
75
|
reason: z.ZodString;
|
|
76
76
|
confidence: z.ZodEnum<["high", "medium", "low"]>;
|
|
77
77
|
}, "strip", z.ZodTypeAny, {
|
|
78
|
+
confidence: "high" | "medium" | "low";
|
|
78
79
|
reason: string;
|
|
79
80
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
80
|
-
confidence: "high" | "medium" | "low";
|
|
81
81
|
}, {
|
|
82
|
+
confidence: "high" | "medium" | "low";
|
|
82
83
|
reason: string;
|
|
83
84
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
84
|
-
confidence: "high" | "medium" | "low";
|
|
85
85
|
}>, "many">;
|
|
86
86
|
sourceGapIds: z.ZodArray<z.ZodString, "many">;
|
|
87
87
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -97,9 +97,9 @@ export declare const NeutralScenarioSchema: z.ZodObject<{
|
|
|
97
97
|
}[];
|
|
98
98
|
tags: string[];
|
|
99
99
|
recommendations: {
|
|
100
|
+
confidence: "high" | "medium" | "low";
|
|
100
101
|
reason: string;
|
|
101
102
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
102
|
-
confidence: "high" | "medium" | "low";
|
|
103
103
|
}[];
|
|
104
104
|
sourceGapIds: string[];
|
|
105
105
|
targetComponent?: string | undefined;
|
|
@@ -116,9 +116,9 @@ export declare const NeutralScenarioSchema: z.ZodObject<{
|
|
|
116
116
|
}[];
|
|
117
117
|
tags: string[];
|
|
118
118
|
recommendations: {
|
|
119
|
+
confidence: "high" | "medium" | "low";
|
|
119
120
|
reason: string;
|
|
120
121
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
121
|
-
confidence: "high" | "medium" | "low";
|
|
122
122
|
}[];
|
|
123
123
|
sourceGapIds: string[];
|
|
124
124
|
targetComponent?: string | undefined;
|
|
@@ -132,17 +132,17 @@ export declare const GeneratedTestSchema: z.ZodObject<{
|
|
|
132
132
|
outputPath: z.ZodString;
|
|
133
133
|
}, "strip", z.ZodTypeAny, {
|
|
134
134
|
code: string;
|
|
135
|
+
source: "llm" | "template";
|
|
135
136
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
136
137
|
scenarioId: string;
|
|
137
138
|
filename: string;
|
|
138
|
-
source: "llm" | "template";
|
|
139
139
|
outputPath: string;
|
|
140
140
|
}, {
|
|
141
141
|
code: string;
|
|
142
|
+
source: "llm" | "template";
|
|
142
143
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
143
144
|
scenarioId: string;
|
|
144
145
|
filename: string;
|
|
145
|
-
source: "llm" | "template";
|
|
146
146
|
outputPath: string;
|
|
147
147
|
}>;
|
|
148
148
|
export declare const GapAnalysisSchema: z.ZodObject<{
|
|
@@ -199,13 +199,13 @@ export declare const GapAnalysisSchema: z.ZodObject<{
|
|
|
199
199
|
reason: z.ZodString;
|
|
200
200
|
confidence: z.ZodEnum<["high", "medium", "low"]>;
|
|
201
201
|
}, "strip", z.ZodTypeAny, {
|
|
202
|
+
confidence: "high" | "medium" | "low";
|
|
202
203
|
reason: string;
|
|
203
204
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
204
|
-
confidence: "high" | "medium" | "low";
|
|
205
205
|
}, {
|
|
206
|
+
confidence: "high" | "medium" | "low";
|
|
206
207
|
reason: string;
|
|
207
208
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
208
|
-
confidence: "high" | "medium" | "low";
|
|
209
209
|
}>, "many">;
|
|
210
210
|
sourceGapIds: z.ZodArray<z.ZodString, "many">;
|
|
211
211
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -221,9 +221,9 @@ export declare const GapAnalysisSchema: z.ZodObject<{
|
|
|
221
221
|
}[];
|
|
222
222
|
tags: string[];
|
|
223
223
|
recommendations: {
|
|
224
|
+
confidence: "high" | "medium" | "low";
|
|
224
225
|
reason: string;
|
|
225
226
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
226
|
-
confidence: "high" | "medium" | "low";
|
|
227
227
|
}[];
|
|
228
228
|
sourceGapIds: string[];
|
|
229
229
|
targetComponent?: string | undefined;
|
|
@@ -240,9 +240,9 @@ export declare const GapAnalysisSchema: z.ZodObject<{
|
|
|
240
240
|
}[];
|
|
241
241
|
tags: string[];
|
|
242
242
|
recommendations: {
|
|
243
|
+
confidence: "high" | "medium" | "low";
|
|
243
244
|
reason: string;
|
|
244
245
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
245
|
-
confidence: "high" | "medium" | "low";
|
|
246
246
|
}[];
|
|
247
247
|
sourceGapIds: string[];
|
|
248
248
|
targetComponent?: string | undefined;
|
|
@@ -256,17 +256,17 @@ export declare const GapAnalysisSchema: z.ZodObject<{
|
|
|
256
256
|
outputPath: z.ZodString;
|
|
257
257
|
}, "strip", z.ZodTypeAny, {
|
|
258
258
|
code: string;
|
|
259
|
+
source: "llm" | "template";
|
|
259
260
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
260
261
|
scenarioId: string;
|
|
261
262
|
filename: string;
|
|
262
|
-
source: "llm" | "template";
|
|
263
263
|
outputPath: string;
|
|
264
264
|
}, {
|
|
265
265
|
code: string;
|
|
266
|
+
source: "llm" | "template";
|
|
266
267
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
267
268
|
scenarioId: string;
|
|
268
269
|
filename: string;
|
|
269
|
-
source: "llm" | "template";
|
|
270
270
|
outputPath: string;
|
|
271
271
|
}>, "many">;
|
|
272
272
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -295,19 +295,19 @@ export declare const GapAnalysisSchema: z.ZodObject<{
|
|
|
295
295
|
}[];
|
|
296
296
|
tags: string[];
|
|
297
297
|
recommendations: {
|
|
298
|
+
confidence: "high" | "medium" | "low";
|
|
298
299
|
reason: string;
|
|
299
300
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
300
|
-
confidence: "high" | "medium" | "low";
|
|
301
301
|
}[];
|
|
302
302
|
sourceGapIds: string[];
|
|
303
303
|
targetComponent?: string | undefined;
|
|
304
304
|
}[];
|
|
305
305
|
generatedTests: {
|
|
306
306
|
code: string;
|
|
307
|
+
source: "llm" | "template";
|
|
307
308
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
308
309
|
scenarioId: string;
|
|
309
310
|
filename: string;
|
|
310
|
-
source: "llm" | "template";
|
|
311
311
|
outputPath: string;
|
|
312
312
|
}[];
|
|
313
313
|
coverageWarning?: "auth-required" | "budget-exceeded" | "low-coverage" | "navigation-failures" | undefined;
|
|
@@ -337,19 +337,19 @@ export declare const GapAnalysisSchema: z.ZodObject<{
|
|
|
337
337
|
}[];
|
|
338
338
|
tags: string[];
|
|
339
339
|
recommendations: {
|
|
340
|
+
confidence: "high" | "medium" | "low";
|
|
340
341
|
reason: string;
|
|
341
342
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
342
|
-
confidence: "high" | "medium" | "low";
|
|
343
343
|
}[];
|
|
344
344
|
sourceGapIds: string[];
|
|
345
345
|
targetComponent?: string | undefined;
|
|
346
346
|
}[];
|
|
347
347
|
generatedTests: {
|
|
348
348
|
code: string;
|
|
349
|
+
source: "llm" | "template";
|
|
349
350
|
adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
|
|
350
351
|
scenarioId: string;
|
|
351
352
|
filename: string;
|
|
352
|
-
source: "llm" | "template";
|
|
353
353
|
outputPath: string;
|
|
354
354
|
}[];
|
|
355
355
|
coverageWarning?: "auth-required" | "budget-exceeded" | "low-coverage" | "navigation-failures" | undefined;
|
package/dist/schemas/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, type ExplorerType, type AdapterType, type FormLoginAuthConfig, type StorageStateAuthConfig, type AuthConfig, type HarnessConfig, type DetectedAuth, } from './config.schema.js';
|
|
1
|
+
export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, AuthPathRequirementsSchema, AuthPathSchema, AuthExplorationSchema, type ExplorerType, type AdapterType, type FormLoginAuthConfig, type StorageStateAuthConfig, type AuthConfig, type HarnessConfig, type DetectedAuth, type AuthPathRequirements, type AuthPath, type AuthExploration, } from './config.schema.js';
|
|
2
2
|
export { DecisionLogEntrySchema, type DecisionLogEntry, } from './decision-log.schema.js';
|
|
3
3
|
export { RouteInventorySchema, RouteSchema, A11yViolationSchema, BrokenLinkSchema, type RouteInventory, type Route, } from './route-inventory.schema.js';
|
|
4
4
|
export { GapAnalysisSchema, GapSchema, NeutralScenarioSchema, GeneratedTestSchema, TestStepSchema, FrameworkRecommendationSchema, type GapAnalysis, type Gap, type NeutralScenario, type GeneratedTest, type TestStep, type FrameworkRecommendation, } from './gap-analysis.schema.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,QAAQ,EACb,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,KAAK,GACX,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EACd,6BAA6B,EAC7B,KAAK,WAAW,EAChB,KAAK,GAAG,EACR,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,kBAAkB,EAClB,KAAK,YAAY,GAClB,MAAM,2BAA2B,CAAC"}
|
package/dist/schemas/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, } from './config.schema.js';
|
|
1
|
+
export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, AuthPathRequirementsSchema, AuthPathSchema, AuthExplorationSchema, } from './config.schema.js';
|
|
2
2
|
export { DecisionLogEntrySchema, } from './decision-log.schema.js';
|
|
3
3
|
export { RouteInventorySchema, RouteSchema, A11yViolationSchema, BrokenLinkSchema, } from './route-inventory.schema.js';
|
|
4
4
|
export { GapAnalysisSchema, GapSchema, NeutralScenarioSchema, GeneratedTestSchema, TestStepSchema, FrameworkRecommendationSchema, } from './gap-analysis.schema.js';
|
|
@@ -5,12 +5,12 @@ export declare const RepoRouteSchema: z.ZodObject<{
|
|
|
5
5
|
method: z.ZodEnum<["GET", "POST", "PUT", "DELETE", "PATCH", "unknown"]>;
|
|
6
6
|
}, "strip", z.ZodTypeAny, {
|
|
7
7
|
path: string;
|
|
8
|
-
file: string;
|
|
9
8
|
method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
9
|
+
file: string;
|
|
10
10
|
}, {
|
|
11
11
|
path: string;
|
|
12
|
-
file: string;
|
|
13
12
|
method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
13
|
+
file: string;
|
|
14
14
|
}>;
|
|
15
15
|
export declare const TestFileSchema: z.ZodObject<{
|
|
16
16
|
file: z.ZodString;
|
|
@@ -62,12 +62,12 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
62
62
|
method: z.ZodEnum<["GET", "POST", "PUT", "DELETE", "PATCH", "unknown"]>;
|
|
63
63
|
}, "strip", z.ZodTypeAny, {
|
|
64
64
|
path: string;
|
|
65
|
-
file: string;
|
|
66
65
|
method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
66
|
+
file: string;
|
|
67
67
|
}, {
|
|
68
68
|
path: string;
|
|
69
|
-
file: string;
|
|
70
69
|
method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
70
|
+
file: string;
|
|
71
71
|
}>, "many">;
|
|
72
72
|
testFiles: z.ZodArray<z.ZodObject<{
|
|
73
73
|
file: z.ZodString;
|
|
@@ -115,8 +115,8 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
115
115
|
scannedAt: string;
|
|
116
116
|
routes: {
|
|
117
117
|
path: string;
|
|
118
|
-
file: string;
|
|
119
118
|
method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
119
|
+
file: string;
|
|
120
120
|
}[];
|
|
121
121
|
repoPath: string;
|
|
122
122
|
testFiles: {
|
|
@@ -139,8 +139,8 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
139
139
|
scannedAt: string;
|
|
140
140
|
routes: {
|
|
141
141
|
path: string;
|
|
142
|
-
file: string;
|
|
143
142
|
method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
143
|
+
file: string;
|
|
144
144
|
}[];
|
|
145
145
|
repoPath: string;
|
|
146
146
|
testFiles: {
|
|
@@ -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;AA4DhE,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,CAkGtF"}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { launchBrowser } from './browser.js';
|
|
2
|
+
async function waitNetworkIdleBestEffort(page) {
|
|
3
|
+
try {
|
|
4
|
+
await page.waitForLoadState('networkidle', { timeout: 5000 });
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
// best-effort — analytics or polling can prevent networkidle
|
|
8
|
+
}
|
|
9
|
+
}
|
|
2
10
|
const OAUTH_PROVIDERS = [
|
|
3
11
|
{ provider: 'github', patterns: [/github/i, /sign in with github/i] },
|
|
4
12
|
{
|
|
@@ -43,11 +51,12 @@ async function firstTextInputNameForLogin(page) {
|
|
|
43
51
|
return null;
|
|
44
52
|
}
|
|
45
53
|
export async function detectAuth(url, timeoutMs = 15000) {
|
|
46
|
-
const browser = await
|
|
54
|
+
const browser = await launchBrowser();
|
|
47
55
|
try {
|
|
48
56
|
const context = await browser.newContext();
|
|
49
57
|
const page = await context.newPage();
|
|
50
58
|
await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
59
|
+
await waitNetworkIdleBestEffort(page);
|
|
51
60
|
let loginUrl = url;
|
|
52
61
|
const looksLikeLoginPage = /login|sign[- ]?in|auth/i.test(page.url()) ||
|
|
53
62
|
(await page.locator('input[type="password"]').count()) > 0;
|
|
@@ -58,6 +67,7 @@ export async function detectAuth(url, timeoutMs = 15000) {
|
|
|
58
67
|
if (href) {
|
|
59
68
|
loginUrl = new URL(href, url).toString();
|
|
60
69
|
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
70
|
+
await waitNetworkIdleBestEffort(page);
|
|
61
71
|
}
|
|
62
72
|
}
|
|
63
73
|
}
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { AuthExplorationSchema, } from '../schemas/config.schema.js';
|
|
2
|
+
import { launchBrowser } from './browser.js';
|
|
3
|
+
import { BUILT_IN_OAUTH_PROVIDERS } from './oauth-providers.js';
|
|
4
|
+
import { loadUserProviders } from './user-providers.js';
|
|
5
|
+
const MAGIC_LINK_PATTERNS = [
|
|
6
|
+
/email me a (sign[- ]?in )?link/i,
|
|
7
|
+
/sign in with email/i,
|
|
8
|
+
/passwordless/i,
|
|
9
|
+
/we'll send you a link/i,
|
|
10
|
+
];
|
|
11
|
+
async function waitNetworkIdleBestEffort(page) {
|
|
12
|
+
try {
|
|
13
|
+
await page.waitForLoadState('networkidle', { timeout: 5000 });
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// best-effort
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function textLooksLikeOAuthIdpButton(text) {
|
|
20
|
+
const t = text.trim();
|
|
21
|
+
if (t.length === 0 || t.length > 120) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return (/\b(sign in with|log in with|continue with|sign up with)\b/i.test(t) ||
|
|
25
|
+
/^(github|google|microsoft|apple)$/i.test(t));
|
|
26
|
+
}
|
|
27
|
+
function slugifyLabel(text) {
|
|
28
|
+
const s = text
|
|
29
|
+
.trim()
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/\s+/g, '-')
|
|
32
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
33
|
+
.slice(0, 48);
|
|
34
|
+
return s.length > 0 ? s : 'unknown';
|
|
35
|
+
}
|
|
36
|
+
function onLoginishPage(url) {
|
|
37
|
+
return /login|sign[- ]?in|auth|sso|oauth/i.test(new URL(url).pathname + new URL(url).hostname);
|
|
38
|
+
}
|
|
39
|
+
function isHeuristicUnknownSso(text, loginish) {
|
|
40
|
+
const t = text.trim();
|
|
41
|
+
if (!loginish || t.length < 3 || t.length > 80) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (/^(submit|cancel|back|close|next|skip|help|faq)$/i.test(t)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (/\b(sign in with|log in with|continue with)\b/i.test(t)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (/\b(sync|sso|portal|workspace|federation)\b/i.test(t)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
function storageRequirement() {
|
|
56
|
+
return {
|
|
57
|
+
method: 'storage-state',
|
|
58
|
+
instruction: 'OAuth and most SSO flows cannot be scripted. Run `qulib auth init --base-url <app-url>` on this machine, then pass the saved storage state JSON to `analyze` or MCP `analyze_app` as `auth: { type: "storage-state", path: "..." }`.',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function collectVisibleControlTexts(page) {
|
|
62
|
+
return page.evaluate(() => {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const out = [];
|
|
65
|
+
const nodes = document.querySelectorAll('button, a[href], [role="button"]');
|
|
66
|
+
for (const el of nodes) {
|
|
67
|
+
const t = (el.textContent ?? '').trim().replace(/\s+/g, ' ');
|
|
68
|
+
if (!t || t.length > 120) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const style = window.getComputedStyle(el);
|
|
72
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!seen.has(t)) {
|
|
76
|
+
seen.add(t);
|
|
77
|
+
out.push(t);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function buildAllProviders() {
|
|
84
|
+
const builtIn = BUILT_IN_OAUTH_PROVIDERS.map((p) => ({ ...p, source: 'built-in' }));
|
|
85
|
+
const user = loadUserProviders().map((p) => ({ ...p, source: 'user-local' }));
|
|
86
|
+
return [...builtIn, ...user];
|
|
87
|
+
}
|
|
88
|
+
function matchProvider(text, p) {
|
|
89
|
+
return p.patterns.some((re) => re.test(text));
|
|
90
|
+
}
|
|
91
|
+
function oauthConfidence(source, loginish) {
|
|
92
|
+
if (source === 'user-local') {
|
|
93
|
+
return 'high';
|
|
94
|
+
}
|
|
95
|
+
if (source === 'built-in' && loginish) {
|
|
96
|
+
return 'high';
|
|
97
|
+
}
|
|
98
|
+
if (source === 'built-in') {
|
|
99
|
+
return 'medium';
|
|
100
|
+
}
|
|
101
|
+
return 'low';
|
|
102
|
+
}
|
|
103
|
+
async function buildFormPaths(page) {
|
|
104
|
+
const passwordCount = await page.locator('input[type="password"]').count();
|
|
105
|
+
if (passwordCount === 0) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const formType = passwordCount > 1 ? 'form-multi' : 'form-login';
|
|
109
|
+
const fields = await page.evaluate(() => {
|
|
110
|
+
const pwd = document.querySelector('input[type="password"]');
|
|
111
|
+
if (!pwd) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
const form = pwd.closest('form') ?? document.body;
|
|
115
|
+
const out = [];
|
|
116
|
+
const inputs = form.querySelectorAll('input, select, textarea');
|
|
117
|
+
for (const el of inputs) {
|
|
118
|
+
if (!(el instanceof HTMLElement)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const tag = el.tagName.toLowerCase();
|
|
122
|
+
if (tag === 'input') {
|
|
123
|
+
const inp = el;
|
|
124
|
+
const t = (inp.type || 'text').toLowerCase();
|
|
125
|
+
if (['hidden', 'submit', 'button', 'image', 'reset'].includes(t)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let fieldType = 'text';
|
|
129
|
+
if (t === 'password') {
|
|
130
|
+
fieldType = 'password';
|
|
131
|
+
}
|
|
132
|
+
else if (t === 'email') {
|
|
133
|
+
fieldType = 'email';
|
|
134
|
+
}
|
|
135
|
+
else if (t === 'checkbox') {
|
|
136
|
+
fieldType = 'checkbox';
|
|
137
|
+
}
|
|
138
|
+
const id = inp.id;
|
|
139
|
+
let label = inp.getAttribute('aria-label') ?? inp.placeholder ?? inp.name ?? fieldType;
|
|
140
|
+
if (id) {
|
|
141
|
+
const lab = document.querySelector(`label[for="${CSS.escape(id)}"]`);
|
|
142
|
+
if (lab?.textContent) {
|
|
143
|
+
label = lab.textContent.trim();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
out.push({
|
|
147
|
+
name: inp.name || inp.id || fieldType,
|
|
148
|
+
label: label.slice(0, 120),
|
|
149
|
+
type: fieldType,
|
|
150
|
+
observedOptions: [],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else if (tag === 'select') {
|
|
154
|
+
const sel = el;
|
|
155
|
+
const opts = Array.from(sel.options).map((o) => o.text.trim()).filter(Boolean);
|
|
156
|
+
out.push({
|
|
157
|
+
name: sel.name || sel.id || 'select',
|
|
158
|
+
label: (sel.getAttribute('aria-label') ?? sel.name ?? 'select').slice(0, 120),
|
|
159
|
+
type: 'select',
|
|
160
|
+
observedOptions: opts.slice(0, 50),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
});
|
|
166
|
+
const requirements = fields.length > 0
|
|
167
|
+
? { method: 'credentials', fields }
|
|
168
|
+
: {
|
|
169
|
+
method: 'unknown',
|
|
170
|
+
instruction: 'A password field exists but field metadata could not be read. Inspect the page in devtools and configure form-login selectors manually, or use `qulib auth init`.',
|
|
171
|
+
};
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
id: formType === 'form-multi' ? 'form-multi' : 'form-login',
|
|
175
|
+
label: formType === 'form-multi' ? 'Multi-field sign-in form' : 'Username / password form',
|
|
176
|
+
type: formType,
|
|
177
|
+
provider: null,
|
|
178
|
+
source: 'heuristic',
|
|
179
|
+
automatable: requirements.method === 'credentials',
|
|
180
|
+
confidence: requirements.method === 'credentials' ? 'medium' : 'low',
|
|
181
|
+
requirements,
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
export async function exploreAuth(url, timeoutMs = 20000) {
|
|
186
|
+
const browser = await launchBrowser();
|
|
187
|
+
try {
|
|
188
|
+
const context = await browser.newContext();
|
|
189
|
+
const page = await context.newPage();
|
|
190
|
+
await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
191
|
+
await waitNetworkIdleBestEffort(page);
|
|
192
|
+
const loginishAfterFirst = /login|sign[- ]?in|auth/i.test(page.url()) || (await page.locator('input[type="password"]').count()) > 0;
|
|
193
|
+
if (!loginishAfterFirst) {
|
|
194
|
+
const loginLink = page.locator('a').filter({ hasText: /^(log ?in|sign ?in|sign in)$/i }).first();
|
|
195
|
+
if ((await loginLink.count()) > 0) {
|
|
196
|
+
const href = await loginLink.getAttribute('href');
|
|
197
|
+
if (href) {
|
|
198
|
+
const next = new URL(href, url).toString();
|
|
199
|
+
await page.goto(next, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
200
|
+
await waitNetworkIdleBestEffort(page);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const finalUrl = page.url();
|
|
205
|
+
const loginish = onLoginishPage(finalUrl) || (await page.locator('input[type="password"]').count()) > 0;
|
|
206
|
+
const allProviders = buildAllProviders();
|
|
207
|
+
const texts = await collectVisibleControlTexts(page);
|
|
208
|
+
const consumed = new Set();
|
|
209
|
+
const authPaths = [];
|
|
210
|
+
const unrecognizedButtons = [];
|
|
211
|
+
for (const rawText of texts) {
|
|
212
|
+
const text = rawText.trim();
|
|
213
|
+
if (!text) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
let matched = null;
|
|
217
|
+
for (const p of allProviders) {
|
|
218
|
+
if (!matchProvider(text, p)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (p.source === 'built-in' && !(textLooksLikeOAuthIdpButton(text) || loginish)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
matched = { p, gate: textLooksLikeOAuthIdpButton(text) || loginish };
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
if (matched) {
|
|
228
|
+
const { p, gate } = matched;
|
|
229
|
+
const id = `oauth:${p.id}`;
|
|
230
|
+
if (consumed.has(id)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
consumed.add(id);
|
|
234
|
+
authPaths.push({
|
|
235
|
+
id,
|
|
236
|
+
label: p.label,
|
|
237
|
+
type: 'oauth',
|
|
238
|
+
provider: p.id,
|
|
239
|
+
source: p.source,
|
|
240
|
+
automatable: false,
|
|
241
|
+
confidence: oauthConfidence(p.source, loginish || gate),
|
|
242
|
+
requirements: storageRequirement(),
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (isHeuristicUnknownSso(text, loginish)) {
|
|
247
|
+
const slug = slugifyLabel(text);
|
|
248
|
+
const id = `oauth-unknown:${slug}`;
|
|
249
|
+
if (consumed.has(id)) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
consumed.add(id);
|
|
253
|
+
authPaths.push({
|
|
254
|
+
id,
|
|
255
|
+
label: text.slice(0, 100),
|
|
256
|
+
type: 'oauth-unknown',
|
|
257
|
+
provider: null,
|
|
258
|
+
source: 'heuristic',
|
|
259
|
+
automatable: false,
|
|
260
|
+
confidence: 'low',
|
|
261
|
+
requirements: storageRequirement(),
|
|
262
|
+
});
|
|
263
|
+
const safePattern = text.slice(0, 48).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
264
|
+
unrecognizedButtons.push({
|
|
265
|
+
label: text.slice(0, 100),
|
|
266
|
+
hint: `If this is your org SSO, register it: qulib auth providers add --id "${slug}" --label "${text.replace(/"/g, '\\"').slice(0, 80)}" --pattern "${safePattern}"`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const pageText = await page.locator('body').innerText().catch(() => '');
|
|
271
|
+
if (MAGIC_LINK_PATTERNS.some((re) => re.test(pageText))) {
|
|
272
|
+
authPaths.push({
|
|
273
|
+
id: 'magic-link',
|
|
274
|
+
label: 'Magic link / passwordless',
|
|
275
|
+
type: 'magic-link',
|
|
276
|
+
provider: null,
|
|
277
|
+
source: 'heuristic',
|
|
278
|
+
automatable: false,
|
|
279
|
+
confidence: 'medium',
|
|
280
|
+
requirements: {
|
|
281
|
+
method: 'storage-state',
|
|
282
|
+
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
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
authPaths.push(...(await buildFormPaths(page)));
|
|
287
|
+
const authRequired = authPaths.length > 0;
|
|
288
|
+
let authScope = 'none';
|
|
289
|
+
if (authRequired) {
|
|
290
|
+
if (loginish) {
|
|
291
|
+
authScope = 'site-wide';
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
authScope = /login|signin|auth/i.test(new URL(finalUrl).pathname) ? 'site-wide' : 'optional';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const suggestedParts = [];
|
|
298
|
+
if (authPaths.some((p) => p.type === 'oauth' || p.type === 'oauth-unknown')) {
|
|
299
|
+
suggestedParts.push('For OAuth or unrecognized SSO buttons, collect a Playwright storage state with `qulib auth init` before calling `analyze_app`.');
|
|
300
|
+
}
|
|
301
|
+
if (authPaths.some((p) => p.type === 'form-login' || p.type === 'form-multi')) {
|
|
302
|
+
suggestedParts.push('For password forms, gather username/password and stable selectors (or use storage state if MFA applies).');
|
|
303
|
+
}
|
|
304
|
+
if (authPaths.some((p) => p.type === 'magic-link')) {
|
|
305
|
+
suggestedParts.push('For magic-link, use `qulib auth init` after the user completes email delivery.');
|
|
306
|
+
}
|
|
307
|
+
if (!authRequired) {
|
|
308
|
+
suggestedParts.push('No sign-in surface detected at this URL; you can run `analyze_app` without auth unless gated deeper in the app.');
|
|
309
|
+
}
|
|
310
|
+
const exploration = {
|
|
311
|
+
url: finalUrl,
|
|
312
|
+
authRequired,
|
|
313
|
+
authScope,
|
|
314
|
+
authPaths,
|
|
315
|
+
observedAt: new Date().toISOString(),
|
|
316
|
+
suggestedAgentBehavior: suggestedParts.join(' '),
|
|
317
|
+
unrecognizedButtons,
|
|
318
|
+
};
|
|
319
|
+
return AuthExplorationSchema.parse(exploration);
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
await browser.close();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/tools/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE1D,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAYtD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { chromium } from '@playwright/test';
|
|
2
|
+
export async function launchBrowser() {
|
|
3
|
+
try {
|
|
4
|
+
return await chromium.launch({ headless: true });
|
|
5
|
+
}
|
|
6
|
+
catch (err) {
|
|
7
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8
|
+
if (message.includes("Executable doesn't exist") || message.includes('chromium')) {
|
|
9
|
+
throw new Error(`Playwright Chromium browser is not installed. Run:\n\n npx playwright install chromium\n\nThen retry your qulib command. This is a one-time setup step.`);
|
|
10
|
+
}
|
|
11
|
+
throw err;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-providers.d.ts","sourceRoot":"","sources":["../../src/tools/oauth-providers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,wBAAwB,EAAE,aAAa,EAsBnD,CAAC"}
|