@qulib/core 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -8
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +86 -7
- package/dist/cli/auth-login-resolve.d.ts +14 -0
- package/dist/cli/auth-login-resolve.d.ts.map +1 -0
- package/dist/cli/auth-login-resolve.js +68 -0
- package/dist/cli/auth-login-run.d.ts +13 -0
- package/dist/cli/auth-login-run.d.ts.map +1 -0
- package/dist/cli/auth-login-run.js +152 -0
- package/dist/cli/index.js +60 -7
- package/dist/harness/state-manager.d.ts +10 -0
- package/dist/harness/state-manager.d.ts.map +1 -1
- package/dist/harness/state-manager.js +15 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/phases/act.js +3 -3
- package/dist/phases/observe.js +5 -5
- package/dist/phases/think.js +1 -1
- package/dist/schemas/automation-maturity.schema.d.ts +40 -0
- package/dist/schemas/automation-maturity.schema.d.ts.map +1 -1
- package/dist/schemas/automation-maturity.schema.js +27 -0
- 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 +22 -0
- package/dist/schemas/repo-analysis.schema.d.ts.map +1 -1
- package/dist/schemas/repo-analysis.schema.js +1 -0
- package/dist/telemetry/emit.d.ts +22 -0
- package/dist/telemetry/emit.d.ts.map +1 -1
- package/dist/telemetry/emit.js +37 -0
- package/dist/telemetry/telemetry.interface.d.ts +1 -1
- package/dist/telemetry/telemetry.interface.d.ts.map +1 -1
- package/dist/tools/apply-auth.d.ts +4 -0
- package/dist/tools/apply-auth.d.ts.map +1 -0
- package/dist/tools/apply-auth.js +35 -0
- package/dist/tools/auth/apply.d.ts +4 -0
- package/dist/tools/auth/apply.d.ts.map +1 -0
- package/dist/tools/auth/apply.js +35 -0
- package/dist/tools/auth/block-gap.d.ts +9 -0
- package/dist/tools/auth/block-gap.d.ts.map +1 -0
- package/dist/tools/auth/block-gap.js +52 -0
- package/dist/tools/auth/custom-providers.d.ts +15 -0
- package/dist/tools/auth/custom-providers.d.ts.map +1 -0
- package/dist/tools/auth/custom-providers.js +62 -0
- package/dist/tools/auth/detect.d.ts +23 -0
- package/dist/tools/auth/detect.d.ts.map +1 -0
- package/dist/tools/auth/detect.js +526 -0
- package/dist/tools/auth/detector.d.ts +23 -0
- package/dist/tools/auth/detector.d.ts.map +1 -0
- package/dist/tools/auth/detector.js +526 -0
- package/dist/tools/auth/explore.d.ts +4 -0
- package/dist/tools/auth/explore.d.ts.map +1 -0
- package/dist/tools/auth/explore.js +346 -0
- package/dist/tools/auth/explorer.d.ts +4 -0
- package/dist/tools/auth/explorer.d.ts.map +1 -0
- package/dist/tools/auth/explorer.js +346 -0
- package/dist/tools/auth/gaps.d.ts +9 -0
- package/dist/tools/auth/gaps.d.ts.map +1 -0
- package/dist/tools/auth/gaps.js +52 -0
- package/dist/tools/auth/oauth-providers.d.ts +7 -0
- package/dist/tools/auth/oauth-providers.d.ts.map +1 -0
- package/dist/tools/auth/oauth-providers.js +21 -0
- package/dist/tools/auth/providers.d.ts +7 -0
- package/dist/tools/auth/providers.d.ts.map +1 -0
- package/dist/tools/auth/providers.js +21 -0
- package/dist/tools/auth/surface-analyzer.d.ts +4 -0
- package/dist/tools/auth/surface-analyzer.d.ts.map +1 -0
- package/dist/tools/auth/surface-analyzer.js +170 -0
- package/dist/tools/auth/surface.d.ts +4 -0
- package/dist/tools/auth/surface.d.ts.map +1 -0
- package/dist/tools/auth/surface.js +170 -0
- package/dist/tools/auth/user-providers.d.ts +15 -0
- package/dist/tools/auth/user-providers.d.ts.map +1 -0
- package/dist/tools/auth/user-providers.js +62 -0
- package/dist/tools/auth-block-gap.d.ts +6 -0
- package/dist/tools/auth-block-gap.d.ts.map +1 -1
- package/dist/tools/auth-block-gap.js +42 -9
- package/dist/tools/auth-detector.d.ts +19 -0
- package/dist/tools/auth-detector.d.ts.map +1 -1
- package/dist/tools/auth-detector.js +186 -8
- package/dist/tools/automation-maturity.d.ts.map +1 -1
- package/dist/tools/automation-maturity.js +76 -20
- package/dist/tools/explorers/browser.d.ts +3 -0
- package/dist/tools/explorers/browser.d.ts.map +1 -0
- package/dist/tools/explorers/browser.js +13 -0
- package/dist/tools/explorers/cypress-explorer.d.ts +8 -0
- package/dist/tools/explorers/cypress-explorer.d.ts.map +1 -0
- package/dist/tools/explorers/cypress-explorer.js +5 -0
- package/dist/tools/explorers/cypress.d.ts +8 -0
- package/dist/tools/explorers/cypress.d.ts.map +1 -0
- package/dist/tools/explorers/cypress.js +5 -0
- package/dist/tools/explorers/explorer.interface.d.ts +7 -0
- package/dist/tools/explorers/explorer.interface.d.ts.map +1 -0
- package/dist/tools/explorers/explorer.interface.js +1 -0
- package/dist/tools/explorers/factory.d.ts +4 -0
- package/dist/tools/explorers/factory.d.ts.map +1 -0
- package/dist/tools/explorers/factory.js +12 -0
- package/dist/tools/explorers/playwright-explorer.d.ts +8 -0
- package/dist/tools/explorers/playwright-explorer.d.ts.map +1 -0
- package/dist/tools/explorers/playwright-explorer.js +172 -0
- package/dist/tools/explorers/playwright.d.ts +8 -0
- package/dist/tools/explorers/playwright.d.ts.map +1 -0
- package/dist/tools/explorers/playwright.js +172 -0
- package/dist/tools/explorers/types.d.ts +7 -0
- package/dist/tools/explorers/types.d.ts.map +1 -0
- package/dist/tools/explorers/types.js +1 -0
- package/dist/tools/playwright-explorer.js +1 -1
- package/dist/tools/repo/detect-framework.d.ts +15 -0
- package/dist/tools/repo/detect-framework.d.ts.map +1 -0
- package/dist/tools/repo/detect-framework.js +153 -0
- package/dist/tools/repo/framework-detector.d.ts +15 -0
- package/dist/tools/repo/framework-detector.d.ts.map +1 -0
- package/dist/tools/repo/framework-detector.js +153 -0
- package/dist/tools/repo/scan.d.ts +19 -0
- package/dist/tools/repo/scan.d.ts.map +1 -0
- package/dist/tools/repo/scan.js +181 -0
- package/dist/tools/repo/scanner.d.ts +19 -0
- package/dist/tools/repo/scanner.d.ts.map +1 -0
- package/dist/tools/repo/scanner.js +181 -0
- package/dist/tools/repo-scanner.d.ts.map +1 -1
- package/dist/tools/repo-scanner.js +7 -2
- package/dist/tools/scoring/automation-maturity.d.ts +4 -0
- package/dist/tools/scoring/automation-maturity.d.ts.map +1 -0
- package/dist/tools/scoring/automation-maturity.js +219 -0
- package/dist/tools/scoring/gap-engine.d.ts +8 -0
- package/dist/tools/scoring/gap-engine.d.ts.map +1 -0
- package/dist/tools/scoring/gap-engine.js +138 -0
- package/dist/tools/scoring/gaps.d.ts +8 -0
- package/dist/tools/scoring/gaps.d.ts.map +1 -0
- package/dist/tools/scoring/gaps.js +138 -0
- package/dist/tools/scoring/public-surface.d.ts +5 -0
- package/dist/tools/scoring/public-surface.d.ts.map +1 -0
- package/dist/tools/scoring/public-surface.js +13 -0
- package/package.json +3 -3
|
@@ -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=surface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/surface.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,sCAAsC,CAAC;AAWhE,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,YAAY,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,EAAE,CAAC,CAwKhB"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { launchBrowser } from '../explorers/browser.js';
|
|
3
|
+
async function waitNetworkIdleBestEffort(page) {
|
|
4
|
+
try {
|
|
5
|
+
await page.waitForLoadState('networkidle', { timeout: 5000 });
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
/* best-effort */
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export async function analyzeAuthSurfaceGaps(url, detection, timeoutMs) {
|
|
12
|
+
if (!detection.hasAuth) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const gaps = [];
|
|
16
|
+
const browser = await launchBrowser();
|
|
17
|
+
try {
|
|
18
|
+
const context = await browser.newContext();
|
|
19
|
+
const page = await context.newPage();
|
|
20
|
+
const resp = await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' }).catch(() => null);
|
|
21
|
+
if (!resp || !resp.ok()) {
|
|
22
|
+
gaps.push({
|
|
23
|
+
id: randomUUID(),
|
|
24
|
+
path: new URL(url).pathname || '/',
|
|
25
|
+
severity: 'critical',
|
|
26
|
+
category: 'auth-surface',
|
|
27
|
+
reason: 'Sign-in surface did not load successfully for evaluation.',
|
|
28
|
+
description: 'The auth entry URL failed to load or returned a non-OK status before DOM checks could run.',
|
|
29
|
+
recommendation: 'Verify DNS, TLS, and that the URL is reachable from the scan environment.',
|
|
30
|
+
});
|
|
31
|
+
return gaps;
|
|
32
|
+
}
|
|
33
|
+
await waitNetworkIdleBestEffort(page);
|
|
34
|
+
const title = await page.title().catch(() => '');
|
|
35
|
+
if (!title || title.trim().length < 3) {
|
|
36
|
+
gaps.push({
|
|
37
|
+
id: randomUUID(),
|
|
38
|
+
path: '/',
|
|
39
|
+
severity: 'medium',
|
|
40
|
+
category: 'auth-surface',
|
|
41
|
+
reason: 'Missing or trivial document title on the sign-in surface.',
|
|
42
|
+
description: 'Users and assistive tech rely on a meaningful window title.',
|
|
43
|
+
recommendation: 'Set a concise, unique <title> for the login experience.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const metaDesc = await page.locator('meta[name="description"]').getAttribute('content').catch(() => null);
|
|
47
|
+
if (!metaDesc || metaDesc.trim().length < 8) {
|
|
48
|
+
gaps.push({
|
|
49
|
+
id: randomUUID(),
|
|
50
|
+
path: '/',
|
|
51
|
+
severity: 'low',
|
|
52
|
+
category: 'auth-surface',
|
|
53
|
+
reason: 'No meta description on the sign-in surface.',
|
|
54
|
+
description: 'Search and sharing previews benefit from meta description on public entry pages.',
|
|
55
|
+
recommendation: 'Add <meta name="description" content="..."> with a short summary of the product.',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const h1Count = await page.locator('h1').count();
|
|
59
|
+
if (h1Count === 0) {
|
|
60
|
+
gaps.push({
|
|
61
|
+
id: randomUUID(),
|
|
62
|
+
path: '/',
|
|
63
|
+
severity: 'medium',
|
|
64
|
+
category: 'auth-surface',
|
|
65
|
+
reason: 'No visible primary heading (h1) on the sign-in surface.',
|
|
66
|
+
description: 'A primary heading helps users orient on the login page.',
|
|
67
|
+
recommendation: 'Add a single descriptive <h1> for the sign-in view.',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const oauthButtons = page.locator('button, a[href], [role="button"]');
|
|
71
|
+
const n = await oauthButtons.count();
|
|
72
|
+
for (let i = 0; i < Math.min(n, 25); i++) {
|
|
73
|
+
const el = oauthButtons.nth(i);
|
|
74
|
+
const text = ((await el.textContent()) ?? '').trim();
|
|
75
|
+
if (!text || text.length > 120)
|
|
76
|
+
continue;
|
|
77
|
+
const isOAuthish = /google|microsoft|github|apple|sso|sign in with|log in with|continue with|oauth/i.test(text);
|
|
78
|
+
if (!isOAuthish)
|
|
79
|
+
continue;
|
|
80
|
+
const role = await el.getAttribute('role');
|
|
81
|
+
const tag = await el.evaluate((node) => node.tagName.toLowerCase());
|
|
82
|
+
const tabIndex = await el.getAttribute('tabindex');
|
|
83
|
+
const aria = await el.getAttribute('aria-label');
|
|
84
|
+
const keyboardable = tag === 'button' || tag === 'a' || role === 'button';
|
|
85
|
+
const labeled = Boolean(aria && aria.trim().length > 0) || text.length > 0;
|
|
86
|
+
if (!keyboardable || tabIndex === '-1') {
|
|
87
|
+
gaps.push({
|
|
88
|
+
id: randomUUID(),
|
|
89
|
+
path: '/',
|
|
90
|
+
severity: 'high',
|
|
91
|
+
category: 'auth-surface',
|
|
92
|
+
reason: `OAuth control "${text.slice(0, 60)}" may not be keyboard-accessible.`,
|
|
93
|
+
description: 'SSO entry points should be real buttons or links with focus support.',
|
|
94
|
+
recommendation: 'Use <button> or <a href> with visible label; avoid tabindex=-1 on the only sign-in path.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else if (!labeled) {
|
|
98
|
+
gaps.push({
|
|
99
|
+
id: randomUUID(),
|
|
100
|
+
path: '/',
|
|
101
|
+
severity: 'medium',
|
|
102
|
+
category: 'auth-surface',
|
|
103
|
+
reason: `OAuth control "${text.slice(0, 60)}" lacks aria-label and has weak visible text.`,
|
|
104
|
+
description: 'Assistive technologies need a clear accessible name for IdP buttons.',
|
|
105
|
+
recommendation: 'Add aria-label or visible text that names the provider and action.',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const hasPassword = (await page.locator('input[type="password"]').count()) > 0;
|
|
110
|
+
const hasEmailLink = await page.getByText(/magic link|email.*link|passwordless/i).count();
|
|
111
|
+
const hasOAuthUi = detection.oauthButtons.length > 0 ||
|
|
112
|
+
(await page.getByText(/sign in with|continue with google|microsoft|github/i).count()) > 0;
|
|
113
|
+
const formLoginFallbacks = (detection.authOptions ?? []).filter((o) => o.type === 'form-login');
|
|
114
|
+
const hasFormLoginFallback = formLoginFallbacks.length > 0;
|
|
115
|
+
if (detection.type === 'oauth' && hasOAuthUi && !hasPassword && !hasEmailLink) {
|
|
116
|
+
if (hasFormLoginFallback) {
|
|
117
|
+
const labels = formLoginFallbacks.map((o) => o.label).join(', ');
|
|
118
|
+
gaps.push({
|
|
119
|
+
id: randomUUID(),
|
|
120
|
+
path: '/',
|
|
121
|
+
severity: 'low',
|
|
122
|
+
category: 'auth-surface',
|
|
123
|
+
reason: `OAuth-primary login with form-login fallback detected via: ${labels}`,
|
|
124
|
+
description: 'A form-based login path exists alongside OAuth. Automate via type="form-login" using the selectors in authOptions.',
|
|
125
|
+
recommendation: `Automatable form option(s): ${labels}. Configure type="form-login" with credentials and selectors from detectedAuth.authOptions.`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
gaps.push({
|
|
130
|
+
id: randomUUID(),
|
|
131
|
+
path: '/',
|
|
132
|
+
severity: 'medium',
|
|
133
|
+
category: 'auth-surface',
|
|
134
|
+
reason: 'OAuth-only entry with no visible password or magic-link fallback.',
|
|
135
|
+
description: 'Users who cannot use a social IdP need another path (email/password, help, or support).',
|
|
136
|
+
recommendation: 'Add a documented fallback (email/password, help desk link, or alternate IdP).',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const errorSelectors = '[role="alert"], [data-testid*="error" i], .error, .alert-danger, [class*="error" i]';
|
|
141
|
+
const errCount = await page.locator(errorSelectors).count();
|
|
142
|
+
if (errCount === 0 && hasOAuthUi) {
|
|
143
|
+
gaps.push({
|
|
144
|
+
id: randomUUID(),
|
|
145
|
+
path: '/',
|
|
146
|
+
severity: 'low',
|
|
147
|
+
category: 'auth-surface',
|
|
148
|
+
reason: 'No obvious in-DOM error container found for OAuth sign-in failures.',
|
|
149
|
+
description: 'IdP failures should surface recoverable feedback in the page.',
|
|
150
|
+
recommendation: 'Reserve a live region or inline alert for OAuth errors returned from the provider.',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const help = await page.getByText(/forgot password|need help|contact support|get help/i).count();
|
|
154
|
+
if (help === 0 && hasOAuthUi) {
|
|
155
|
+
gaps.push({
|
|
156
|
+
id: randomUUID(),
|
|
157
|
+
path: '/',
|
|
158
|
+
severity: 'low',
|
|
159
|
+
category: 'auth-surface',
|
|
160
|
+
reason: 'No visible “forgot password” or help path detected near OAuth controls.',
|
|
161
|
+
description: 'Users locked out of an IdP need a support or recovery affordance.',
|
|
162
|
+
recommendation: 'Link to account recovery, IT help, or a support URL near the sign-in actions.',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await browser.close();
|
|
168
|
+
}
|
|
169
|
+
return gaps;
|
|
170
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { OAuthProvider } from './oauth-providers.js';
|
|
2
|
+
export interface SerializedProvider {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
patterns: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function loadUserProviders(): OAuthProvider[];
|
|
8
|
+
export declare function addUserProvider(input: {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
pattern: string;
|
|
12
|
+
}): void;
|
|
13
|
+
export declare function removeUserProvider(id: string): boolean;
|
|
14
|
+
export declare function listUserProviders(): SerializedProvider[];
|
|
15
|
+
//# sourceMappingURL=user-providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-providers.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/user-providers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI1D,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,iBAAiB,IAAI,aAAa,EAAE,CAOnD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAc3F;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAStD;AAED,wBAAgB,iBAAiB,IAAI,kBAAkB,EAAE,CAExD"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
const USER_PROVIDERS_PATH = join(homedir(), '.qulib', 'providers.json');
|
|
5
|
+
export function loadUserProviders() {
|
|
6
|
+
const raw = loadSerialized();
|
|
7
|
+
return raw.map((p) => ({
|
|
8
|
+
id: p.id,
|
|
9
|
+
label: p.label,
|
|
10
|
+
patterns: p.patterns.map((src) => new RegExp(src, 'i')),
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
export function addUserProvider(input) {
|
|
14
|
+
const existing = loadSerialized();
|
|
15
|
+
const idx = existing.findIndex((p) => p.id === input.id);
|
|
16
|
+
if (idx >= 0) {
|
|
17
|
+
const p = existing[idx];
|
|
18
|
+
if (!p.patterns.includes(input.pattern)) {
|
|
19
|
+
p.patterns.push(input.pattern);
|
|
20
|
+
}
|
|
21
|
+
p.label = input.label;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
existing.push({ id: input.id, label: input.label, patterns: [input.pattern] });
|
|
25
|
+
}
|
|
26
|
+
ensureDir();
|
|
27
|
+
writeFileSync(USER_PROVIDERS_PATH, JSON.stringify(existing, null, 2), 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
export function removeUserProvider(id) {
|
|
30
|
+
const existing = loadSerialized();
|
|
31
|
+
const filtered = existing.filter((p) => p.id !== id);
|
|
32
|
+
if (filtered.length === existing.length) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
ensureDir();
|
|
36
|
+
writeFileSync(USER_PROVIDERS_PATH, JSON.stringify(filtered, null, 2), 'utf-8');
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
export function listUserProviders() {
|
|
40
|
+
return loadSerialized();
|
|
41
|
+
}
|
|
42
|
+
function loadSerialized() {
|
|
43
|
+
if (!existsSync(USER_PROVIDERS_PATH)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(readFileSync(USER_PROVIDERS_PATH, 'utf-8'));
|
|
48
|
+
if (!Array.isArray(parsed)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function ensureDir() {
|
|
58
|
+
const dir = dirname(USER_PROVIDERS_PATH);
|
|
59
|
+
if (!existsSync(dir)) {
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import type { Gap } from '../schemas/gap-analysis.schema.js';
|
|
2
|
+
import type { StorageStateInvalidReason } from './auth-detector.js';
|
|
2
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;
|
|
3
9
|
//# sourceMappingURL=auth-block-gap.d.ts.map
|
|
@@ -1 +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;
|
|
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;AAC7D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAmBpE,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,12 +1,23 @@
|
|
|
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
|
+
}
|
|
1
18
|
export function buildAuthBlockGap(url) {
|
|
2
|
-
const host = (
|
|
3
|
-
|
|
4
|
-
return new URL(url).hostname;
|
|
5
|
-
}
|
|
6
|
-
catch {
|
|
7
|
-
return url;
|
|
8
|
-
}
|
|
9
|
-
})();
|
|
19
|
+
const host = safeHost(url);
|
|
20
|
+
const safeUrl = safeOriginAndPath(url);
|
|
10
21
|
return {
|
|
11
22
|
id: 'auth-block',
|
|
12
23
|
path: '/',
|
|
@@ -14,6 +25,28 @@ export function buildAuthBlockGap(url) {
|
|
|
14
25
|
category: 'coverage',
|
|
15
26
|
reason: `Scan blocked by authentication. No authenticated pages were evaluated for ${host}.`,
|
|
16
27
|
description: 'Scan blocked by authentication. 0 authenticated pages were evaluated.',
|
|
17
|
-
recommendation: `Run \`qulib auth init --base-url ${
|
|
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],
|
|
18
51
|
};
|
|
19
52
|
}
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
1
2
|
import type { DetectedAuth } from '../schemas/config.schema.js';
|
|
2
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>;
|
|
3
22
|
export declare function detectAuth(url: string, timeoutMs?: number, progress?: AnalyzeProgressSink): Promise<DetectedAuth>;
|
|
4
23
|
//# 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":"
|
|
1
|
+
{"version":3,"file":"auth-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,6BAA6B,CAAC;AAC1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAItE,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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
1
3
|
import { launchBrowser } from './browser.js';
|
|
2
4
|
import { BUILT_IN_OAUTH_PROVIDERS } from './oauth-providers.js';
|
|
3
5
|
async function waitNetworkIdleBestEffort(page) {
|
|
@@ -166,6 +168,7 @@ async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, time
|
|
|
166
168
|
continue;
|
|
167
169
|
seenLabels.add(label);
|
|
168
170
|
candidateAttempts += 1;
|
|
171
|
+
const originBefore = new URL(page.url()).origin;
|
|
169
172
|
if (debugAuth()) {
|
|
170
173
|
progress?.debug(`detect_auth click-reveal try label="${label.slice(0, 80)}"`);
|
|
171
174
|
}
|
|
@@ -191,7 +194,21 @@ async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, time
|
|
|
191
194
|
continue;
|
|
192
195
|
}
|
|
193
196
|
try {
|
|
194
|
-
await page.
|
|
197
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 5000 });
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
/* best-effort after navigation */
|
|
201
|
+
}
|
|
202
|
+
if (new URL(page.url()).origin !== originBefore) {
|
|
203
|
+
if (debugAuth()) {
|
|
204
|
+
progress?.debug(`detect_auth click-reveal aborted (cross-origin after click): was ${originBefore} now ${new URL(page.url()).origin}`);
|
|
205
|
+
}
|
|
206
|
+
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
207
|
+
await waitNetworkIdleBestEffort(page);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
await page.locator('input[type="password"]:visible').first().waitFor({ state: 'visible', timeout: 2000 });
|
|
195
212
|
}
|
|
196
213
|
catch {
|
|
197
214
|
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
@@ -218,6 +235,161 @@ async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, time
|
|
|
218
235
|
}
|
|
219
236
|
return out;
|
|
220
237
|
}
|
|
238
|
+
export function evaluateStorageStateValidity(signals) {
|
|
239
|
+
let finalOrigin = null;
|
|
240
|
+
try {
|
|
241
|
+
finalOrigin = new URL(signals.finalUrl).origin;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
finalOrigin = null;
|
|
245
|
+
}
|
|
246
|
+
if (finalOrigin === null || finalOrigin !== signals.expectedOrigin) {
|
|
247
|
+
return {
|
|
248
|
+
valid: false,
|
|
249
|
+
reasonCode: 'wrong-origin',
|
|
250
|
+
reason: 'session redirected to a different origin than the target app',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (signals.visiblePasswordCount > 0) {
|
|
254
|
+
return {
|
|
255
|
+
valid: false,
|
|
256
|
+
reasonCode: 'expired-or-unauthorized',
|
|
257
|
+
reason: 'login form still visible after loading storage state (session likely expired)',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (signals.hadUnauthorizedHttp) {
|
|
261
|
+
return {
|
|
262
|
+
valid: false,
|
|
263
|
+
reasonCode: 'expired-or-unauthorized',
|
|
264
|
+
reason: 'HTTP 401/403 on authenticated request (session likely expired or invalid)',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return { valid: true, reasonCode: 'ok', reason: 'session appears active' };
|
|
268
|
+
}
|
|
269
|
+
export async function preflightStorageStateFile(storagePath) {
|
|
270
|
+
let exists = false;
|
|
271
|
+
try {
|
|
272
|
+
const s = await stat(storagePath);
|
|
273
|
+
exists = s.isFile();
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
exists = false;
|
|
277
|
+
}
|
|
278
|
+
if (!exists) {
|
|
279
|
+
return {
|
|
280
|
+
valid: false,
|
|
281
|
+
reasonCode: 'missing-file',
|
|
282
|
+
reason: 'storage state file does not exist',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
let raw;
|
|
286
|
+
try {
|
|
287
|
+
raw = await readFile(storagePath, 'utf8');
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return {
|
|
291
|
+
valid: false,
|
|
292
|
+
reasonCode: 'unreadable-file',
|
|
293
|
+
reason: 'storage state file is not readable',
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
let parsed;
|
|
297
|
+
try {
|
|
298
|
+
parsed = JSON.parse(raw);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return {
|
|
302
|
+
valid: false,
|
|
303
|
+
reasonCode: 'invalid-json',
|
|
304
|
+
reason: 'storage state file is not valid JSON',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const cookieCount = Array.isArray(parsed?.cookies) ? parsed.cookies.length : 0;
|
|
308
|
+
const originEntries = Array.isArray(parsed?.origins) ? parsed.origins : [];
|
|
309
|
+
const localStorageEntryCount = originEntries.reduce((sum, entry) => {
|
|
310
|
+
return sum + (Array.isArray(entry?.localStorage) ? entry.localStorage.length : 0);
|
|
311
|
+
}, 0);
|
|
312
|
+
if (cookieCount === 0 && localStorageEntryCount === 0) {
|
|
313
|
+
return {
|
|
314
|
+
valid: false,
|
|
315
|
+
reasonCode: 'no-auth-cookies',
|
|
316
|
+
reason: 'storage state file contains no cookies and no localStorage entries',
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
export async function waitForReturnToOrigin(page, baseUrl, timeoutMs = 15000) {
|
|
322
|
+
const targetOrigin = new URL(baseUrl).origin;
|
|
323
|
+
const deadline = Date.now() + timeoutMs;
|
|
324
|
+
while (Date.now() < deadline) {
|
|
325
|
+
const finalUrl = page.url();
|
|
326
|
+
try {
|
|
327
|
+
if (new URL(finalUrl).origin === targetOrigin) {
|
|
328
|
+
return { returned: true, finalUrl };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
/* ignore transient invalid URLs */
|
|
333
|
+
}
|
|
334
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
335
|
+
}
|
|
336
|
+
return { returned: false, finalUrl: page.url() };
|
|
337
|
+
}
|
|
338
|
+
export async function validateStorageState(url, storagePath, timeoutMs = 10000) {
|
|
339
|
+
let expectedOrigin;
|
|
340
|
+
try {
|
|
341
|
+
expectedOrigin = new URL(url).origin;
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return {
|
|
345
|
+
valid: false,
|
|
346
|
+
reasonCode: 'unknown',
|
|
347
|
+
reason: 'target URL could not be parsed for origin matching',
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const storageAbs = resolve(process.cwd(), storagePath);
|
|
351
|
+
const preflight = await preflightStorageStateFile(storageAbs);
|
|
352
|
+
if (preflight !== null) {
|
|
353
|
+
return preflight;
|
|
354
|
+
}
|
|
355
|
+
let hadUnauthorizedHttp = false;
|
|
356
|
+
const browser = await launchBrowser();
|
|
357
|
+
try {
|
|
358
|
+
const context = await browser.newContext({ storageState: storageAbs });
|
|
359
|
+
const page = await context.newPage();
|
|
360
|
+
page.on('response', (res) => {
|
|
361
|
+
const s = res.status();
|
|
362
|
+
if (s === 401 || s === 403) {
|
|
363
|
+
hadUnauthorizedHttp = true;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
try {
|
|
367
|
+
await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return {
|
|
371
|
+
valid: false,
|
|
372
|
+
reasonCode: 'unknown',
|
|
373
|
+
reason: 'navigation to target URL failed while validating storage state',
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
await waitNetworkIdleBestEffort(page);
|
|
377
|
+
const finalUrl = page.url();
|
|
378
|
+
const visiblePasswordCount = await page
|
|
379
|
+
.locator('input[type="password"]:visible')
|
|
380
|
+
.count()
|
|
381
|
+
.catch(() => 0);
|
|
382
|
+
return evaluateStorageStateValidity({
|
|
383
|
+
expectedOrigin,
|
|
384
|
+
finalUrl,
|
|
385
|
+
visiblePasswordCount,
|
|
386
|
+
hadUnauthorizedHttp,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
finally {
|
|
390
|
+
await browser.close();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
221
393
|
export async function detectAuth(url, timeoutMs = 15000, progress) {
|
|
222
394
|
const browser = await launchBrowser();
|
|
223
395
|
try {
|
|
@@ -232,7 +404,7 @@ export async function detectAuth(url, timeoutMs = 15000, progress) {
|
|
|
232
404
|
}
|
|
233
405
|
let loginUrl = url;
|
|
234
406
|
const looksLikeLoginPage = /login|sign[- ]?in|auth/i.test(page.url()) ||
|
|
235
|
-
(await page.locator('input[type="password"]').count()) > 0;
|
|
407
|
+
(await page.locator('input[type="password"]:visible').count()) > 0;
|
|
236
408
|
if (!looksLikeLoginPage) {
|
|
237
409
|
const loginLink = page.locator('a').filter({ hasText: /^(log ?in|sign ?in|sign in)$/i }).first();
|
|
238
410
|
const loginLinkCount = await loginLink.count();
|
|
@@ -247,7 +419,7 @@ export async function detectAuth(url, timeoutMs = 15000, progress) {
|
|
|
247
419
|
}
|
|
248
420
|
}
|
|
249
421
|
}
|
|
250
|
-
const passwordInputs = page.locator('input[type="password"]');
|
|
422
|
+
const passwordInputs = page.locator('input[type="password"]:visible');
|
|
251
423
|
const passwordCount = await passwordInputs.count();
|
|
252
424
|
progress?.debug(`detect_auth selector input[type=password] count=${passwordCount}`);
|
|
253
425
|
const hasFormLogin = passwordCount > 0;
|
|
@@ -274,13 +446,19 @@ export async function detectAuth(url, timeoutMs = 15000, progress) {
|
|
|
274
446
|
matchedAny = true;
|
|
275
447
|
}
|
|
276
448
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
449
|
+
if (!matchedAny) {
|
|
450
|
+
const builtIn = BUILT_IN_OAUTH_PROVIDERS.find((p) => p.label.toLowerCase() === trimmed.toLowerCase());
|
|
451
|
+
if (builtIn) {
|
|
452
|
+
if (!oauthButtons.find((b) => b.provider === builtIn.id)) {
|
|
453
|
+
oauthButtons.push({ provider: builtIn.id, text: trimmed.slice(0, 100) });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
else if (!oauthButtons.find((b) => b.text === trimmed.slice(0, 100))) {
|
|
457
|
+
oauthButtons.push({ provider: 'unknown', text: trimmed.slice(0, 100) });
|
|
458
|
+
}
|
|
280
459
|
}
|
|
281
460
|
}
|
|
282
|
-
|
|
283
|
-
const skipProbeLabels = new Set(oauthButtons.filter((b) => b.provider !== 'unknown').map((b) => b.text.trim()));
|
|
461
|
+
const skipProbeLabels = new Set(oauthButtons.map((b) => b.text.trim()));
|
|
284
462
|
const clickRevealForms = await probeClickToRevealForms(page, loginUrl, skipProbeLabels, timeoutMs, progress);
|
|
285
463
|
const pageText = await page.locator('body').innerText().catch(() => '');
|
|
286
464
|
const hasMagicLink = MAGIC_LINK_PATTERNS.some((p) => p.test(pageText));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automation-maturity.d.ts","sourceRoot":"","sources":["../../src/tools/automation-maturity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"automation-maturity.d.ts","sourceRoot":"","sources":["../../src/tools/automation-maturity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EACV,kBAAkB,EAGnB,MAAM,0CAA0C,CAAC;AAiDlD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,kBAAkB,CAuLhF"}
|