@govtechsg/oobee 0.10.76 → 0.10.78-alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.github/workflows/publish.yml +8 -1
  2. package/INTEGRATION.md +50 -3
  3. package/dist/cli.js +252 -0
  4. package/dist/combine.js +221 -0
  5. package/dist/constants/cliFunctions.js +306 -0
  6. package/dist/constants/common.js +1669 -0
  7. package/dist/constants/constants.js +913 -0
  8. package/dist/constants/errorMeta.json +319 -0
  9. package/dist/constants/itemTypeDescription.js +7 -0
  10. package/dist/constants/oobeeAi.js +121 -0
  11. package/dist/constants/questions.js +151 -0
  12. package/dist/constants/sampleData.js +176 -0
  13. package/dist/crawlers/commonCrawlerFunc.js +428 -0
  14. package/dist/crawlers/crawlDomain.js +613 -0
  15. package/dist/crawlers/crawlIntelligentSitemap.js +135 -0
  16. package/dist/crawlers/crawlLocalFile.js +151 -0
  17. package/dist/crawlers/crawlSitemap.js +303 -0
  18. package/dist/crawlers/custom/escapeCssSelector.js +10 -0
  19. package/dist/crawlers/custom/evaluateAltText.js +11 -0
  20. package/dist/crawlers/custom/extractAndGradeText.js +44 -0
  21. package/dist/crawlers/custom/extractText.js +27 -0
  22. package/dist/crawlers/custom/findElementByCssSelector.js +36 -0
  23. package/dist/crawlers/custom/flagUnlabelledClickableElements.js +963 -0
  24. package/dist/crawlers/custom/framesCheck.js +37 -0
  25. package/dist/crawlers/custom/getAxeConfiguration.js +111 -0
  26. package/dist/crawlers/custom/gradeReadability.js +23 -0
  27. package/dist/crawlers/custom/utils.js +1024 -0
  28. package/dist/crawlers/custom/xPathToCss.js +147 -0
  29. package/dist/crawlers/guards/urlGuard.js +71 -0
  30. package/dist/crawlers/pdfScanFunc.js +276 -0
  31. package/dist/crawlers/runCustom.js +89 -0
  32. package/dist/exclusions.txt +7 -0
  33. package/dist/generateHtmlReport.js +144 -0
  34. package/dist/index.js +62 -0
  35. package/dist/logs.js +84 -0
  36. package/dist/mergeAxeResults.js +1588 -0
  37. package/dist/npmIndex.js +640 -0
  38. package/dist/proxyService.js +360 -0
  39. package/dist/runGenerateJustHtmlReport.js +16 -0
  40. package/dist/screenshotFunc/htmlScreenshotFunc.js +355 -0
  41. package/dist/screenshotFunc/pdfScreenshotFunc.js +645 -0
  42. package/dist/services/s3Uploader.js +127 -0
  43. package/dist/static/ejs/partials/components/allIssues/AllIssues.ejs +9 -0
  44. package/dist/static/ejs/partials/components/allIssues/CategoryBadges.ejs +82 -0
  45. package/dist/static/ejs/partials/components/allIssues/FilterBar.ejs +33 -0
  46. package/dist/static/ejs/partials/components/allIssues/IssuesTable.ejs +41 -0
  47. package/dist/static/ejs/partials/components/header/SiteInfo.ejs +119 -0
  48. package/dist/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +15 -0
  49. package/dist/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +44 -0
  50. package/dist/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +142 -0
  51. package/dist/static/ejs/partials/components/prioritiseIssues/IssueDetailCard.ejs +36 -0
  52. package/dist/static/ejs/partials/components/prioritiseIssues/PrioritiseIssues.ejs +47 -0
  53. package/dist/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +196 -0
  54. package/dist/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +48 -0
  55. package/dist/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
  56. package/dist/static/ejs/partials/components/shared/InfoAlert.ejs +3 -0
  57. package/dist/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
  58. package/dist/static/ejs/partials/components/summaryScanResults.ejs +16 -0
  59. package/dist/static/ejs/partials/components/summaryTable.ejs +20 -0
  60. package/dist/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
  61. package/dist/static/ejs/partials/components/topTen.ejs +6 -0
  62. package/dist/static/ejs/partials/components/wcagCompliance/FailedCriteria.ejs +47 -0
  63. package/dist/static/ejs/partials/components/wcagCompliance/WcagCompliance.ejs +16 -0
  64. package/dist/static/ejs/partials/components/wcagCompliance/WcagGaugeBar.ejs +16 -0
  65. package/dist/static/ejs/partials/components/wcagCoverageDetails.ejs +18 -0
  66. package/dist/static/ejs/partials/footer.ejs +24 -0
  67. package/dist/static/ejs/partials/header.ejs +14 -0
  68. package/dist/static/ejs/partials/main.ejs +29 -0
  69. package/dist/static/ejs/partials/scripts/allIssues/AllIssues.ejs +376 -0
  70. package/dist/static/ejs/partials/scripts/bootstrap.ejs +8 -0
  71. package/dist/static/ejs/partials/scripts/categorySummary.ejs +141 -0
  72. package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
  73. package/dist/static/ejs/partials/scripts/header/SiteInfo.ejs +44 -0
  74. package/dist/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +51 -0
  75. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +127 -0
  76. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanDetails.ejs +60 -0
  77. package/dist/static/ejs/partials/scripts/highlightjs.ejs +335 -0
  78. package/dist/static/ejs/partials/scripts/popper.ejs +7 -0
  79. package/dist/static/ejs/partials/scripts/prioritiseIssues/IssueDetailCard.ejs +137 -0
  80. package/dist/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +214 -0
  81. package/dist/static/ejs/partials/scripts/prioritiseIssues/wcagSvgMap.ejs +861 -0
  82. package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +957 -0
  83. package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +353 -0
  84. package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +468 -0
  85. package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +306 -0
  86. package/dist/static/ejs/partials/scripts/ruleModal/utilities.ejs +483 -0
  87. package/dist/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +35 -0
  88. package/dist/static/ejs/partials/scripts/screenshotLightbox.ejs +75 -0
  89. package/dist/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
  90. package/dist/static/ejs/partials/scripts/summaryTable.ejs +78 -0
  91. package/dist/static/ejs/partials/scripts/topTen.ejs +61 -0
  92. package/dist/static/ejs/partials/scripts/utils.ejs +453 -0
  93. package/dist/static/ejs/partials/scripts/wcagCompliance/FailedCriteria.ejs +103 -0
  94. package/dist/static/ejs/partials/scripts/wcagCompliance/WcagGaugeBar.ejs +47 -0
  95. package/dist/static/ejs/partials/scripts/wcagCompliance.ejs +15 -0
  96. package/dist/static/ejs/partials/scripts/wcagCoverageDetails.ejs +75 -0
  97. package/dist/static/ejs/partials/styles/allIssues/AllIssues.ejs +384 -0
  98. package/dist/static/ejs/partials/styles/bootstrap.ejs +12391 -0
  99. package/dist/static/ejs/partials/styles/header/SiteInfo.ejs +121 -0
  100. package/dist/static/ejs/partials/styles/header/aboutScanModal/AboutScanModal.ejs +82 -0
  101. package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanConfiguration.ejs +50 -0
  102. package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +149 -0
  103. package/dist/static/ejs/partials/styles/header.ejs +7 -0
  104. package/dist/static/ejs/partials/styles/highlightjs.ejs +54 -0
  105. package/dist/static/ejs/partials/styles/prioritiseIssues/IssueDetailCard.ejs +141 -0
  106. package/dist/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +204 -0
  107. package/dist/static/ejs/partials/styles/ruleModal/ruleOffcanvas.ejs +456 -0
  108. package/dist/static/ejs/partials/styles/scannedPagesSegmentedTabs.ejs +46 -0
  109. package/dist/static/ejs/partials/styles/shared/InfoAlert.ejs +12 -0
  110. package/dist/static/ejs/partials/styles/styles.ejs +1607 -0
  111. package/dist/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
  112. package/dist/static/ejs/partials/styles/topTenCard.ejs +44 -0
  113. package/dist/static/ejs/partials/styles/wcagCompliance/FailedCriteria.ejs +59 -0
  114. package/dist/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +62 -0
  115. package/dist/static/ejs/partials/styles/wcagCompliance.ejs +36 -0
  116. package/dist/static/ejs/partials/styles/wcagCoverageDetails.ejs +33 -0
  117. package/dist/static/ejs/partials/summaryHeader.ejs +70 -0
  118. package/dist/static/ejs/partials/summaryMain.ejs +49 -0
  119. package/dist/static/ejs/report.ejs +226 -0
  120. package/dist/static/ejs/summary.ejs +47 -0
  121. package/dist/types/types.js +1 -0
  122. package/dist/utils.js +1070 -0
  123. package/examples/oobee-cypress-integration-js/cypress/support/e2e.js +36 -6
  124. package/examples/oobee-cypress-integration-js/cypress.config.js +45 -1
  125. package/examples/oobee-cypress-integration-ts/cypress.config.ts +47 -1
  126. package/examples/oobee-cypress-integration-ts/src/cypress/support/e2e.ts +36 -6
  127. package/examples/oobee-playwright-integration-js/oobee-playwright-demo.js +2 -1
  128. package/examples/oobee-playwright-integration-ts/src/oobee-playwright-demo.ts +2 -1
  129. package/examples/oobee-scan-html-demo.js +51 -0
  130. package/examples/oobee-scan-page-demo.js +40 -0
  131. package/package.json +9 -3
  132. package/src/constants/common.ts +2 -2
  133. package/src/constants/constants.ts +3 -1
  134. package/src/crawlers/crawlDomain.ts +1 -0
  135. package/src/crawlers/runCustom.ts +0 -1
  136. package/src/mergeAxeResults.ts +43 -22
  137. package/src/npmIndex.ts +500 -131
@@ -0,0 +1,360 @@
1
+ // getProxyInfo.ts
2
+ // Cross-platform proxy detector for Playwright/Chromium with PAC + credentials + macOS Keychain support.
3
+ //
4
+ // Windows: WinINET registry (HKCU → HKLM)
5
+ // macOS: env vars first, then `scutil --proxy` (reads per-protocol usernames/passwords;
6
+ // if password is missing, fetch it from Keychain via `/usr/bin/security`)
7
+ // Linux/others: env vars only
8
+ //
9
+ // Output precedence in proxyInfoToArgs():
10
+ // 1) pacUrl → ["--proxy-pac-url=<url>", ...bypass]
11
+ // 2) manual proxies → ["--proxy-server=...", ...bypass] (embeds creds if provided/available)
12
+ // 3) autoDetect → ["--proxy-auto-detect", ...bypass]
13
+ import os from 'os';
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { spawnSync } from 'child_process';
17
+ /* ============================ helpers ============================ */
18
+ function stripScheme(u) {
19
+ return u.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, '');
20
+ }
21
+ function semiJoin(arr) {
22
+ if (!arr)
23
+ return undefined;
24
+ const cleaned = arr.map(s => s.trim()).filter(Boolean);
25
+ return cleaned.length ? cleaned.join(';') : undefined;
26
+ }
27
+ function readCredsFromEnv() {
28
+ const username = process.env.PROXY_USERNAME || process.env.HTTP_PROXY_USERNAME || undefined;
29
+ const password = process.env.PROXY_PASSWORD || process.env.HTTP_PROXY_PASSWORD || undefined;
30
+ return { username, password };
31
+ }
32
+ function hasUserinfo(v) {
33
+ return !!(v && /@/.test(v.split('/')[0]));
34
+ }
35
+ function pick(map, keys) {
36
+ for (const k of keys) {
37
+ const v = map[k];
38
+ if (v && String(v).trim().length)
39
+ return String(v).trim();
40
+ }
41
+ return undefined;
42
+ }
43
+ function extractHost(valueHostPort) {
44
+ // valueHostPort may be "user:pass@host:port" or "host:port"
45
+ const noUser = valueHostPort.includes('@') ? valueHostPort.split('@', 2)[1] : valueHostPort;
46
+ return noUser.split(':', 2)[0];
47
+ }
48
+ /* ============================ env (macOS + Linux) ============================ */
49
+ function parseEnvProxyCommon() {
50
+ const http = process.env.HTTP_PROXY || process.env.http_proxy || '';
51
+ const https = process.env.HTTPS_PROXY || process.env.https_proxy || '';
52
+ const socks = process.env.ALL_PROXY || process.env.all_proxy || '';
53
+ const noProxy = process.env.NO_PROXY || process.env.no_proxy || '';
54
+ const info = {};
55
+ if (http)
56
+ info.http = stripScheme(http);
57
+ if (https)
58
+ info.https = stripScheme(https);
59
+ if (socks)
60
+ info.socks = stripScheme(socks);
61
+ if (noProxy)
62
+ info.bypassList = semiJoin(noProxy.split(/[,;]/));
63
+ const { username, password } = readCredsFromEnv();
64
+ if (username && password) {
65
+ info.username = username;
66
+ info.password = password;
67
+ }
68
+ return (info.http || info.https || info.socks || info.bypassList) ? info : null;
69
+ }
70
+ /* ============================ macOS Keychain ============================ */
71
+ /**
72
+ * Try to read an Internet Password from macOS Keychain for a given host/account.
73
+ * We intentionally avoid passing any user-controlled strings; host/account come from scutil.
74
+ * Returns the password (raw) or undefined.
75
+ */
76
+ export function keychainFindInternetPassword(host, account) {
77
+ console.log("Attempting to find internet proxy password in macOS keychain...");
78
+ // Only attempt on macOS, in an interactive session, or when explicitly allowed.
79
+ if (process.platform !== 'darwin')
80
+ return undefined;
81
+ const allow = process.stdin.isTTY || process.env.ENABLE_KEYCHAIN_LOOKUP === '1';
82
+ if (!allow)
83
+ return undefined;
84
+ const SECURITY_BIN = '/usr/bin/security';
85
+ const OUTPUT_LIMIT = 64 * 1024; // 64 KiB
86
+ // Verify absolute binary and realpath
87
+ try {
88
+ if (!fs.existsSync(SECURITY_BIN))
89
+ return undefined;
90
+ const real = fs.realpathSync(SECURITY_BIN);
91
+ if (real !== SECURITY_BIN)
92
+ return undefined;
93
+ }
94
+ catch {
95
+ return undefined;
96
+ }
97
+ // Minimal sanitized env (avoid proxy/env influence)
98
+ const env = {
99
+ PATH: '/usr/bin:/bin',
100
+ http_proxy: '', https_proxy: '', all_proxy: '', no_proxy: '',
101
+ HTTP_PROXY: '', HTTPS_PROXY: '', ALL_PROXY: '', NO_PROXY: '',
102
+ NODE_OPTIONS: '', NODE_PATH: '', DYLD_LIBRARY_PATH: '', LD_LIBRARY_PATH: '',
103
+ };
104
+ const baseArgs = ['find-internet-password', '-s', host, '-w'];
105
+ const args = account ? [...baseArgs, '-a', account] : baseArgs;
106
+ // No timeout: allow user to respond to Keychain prompt
107
+ const res = spawnSync(SECURITY_BIN, args, {
108
+ encoding: 'utf8',
109
+ windowsHide: true,
110
+ shell: false,
111
+ env,
112
+ stdio: ['ignore', 'pipe', 'pipe'],
113
+ });
114
+ if (!res.error && res.status === 0 && res.stdout) {
115
+ const out = res.stdout.slice(0, OUTPUT_LIMIT).replace(/\r?\n$/, '');
116
+ return out || undefined;
117
+ }
118
+ // Retry without account if first try used one
119
+ if (account) {
120
+ const retry = spawnSync(SECURITY_BIN, baseArgs, {
121
+ encoding: 'utf8',
122
+ windowsHide: true,
123
+ shell: false,
124
+ env,
125
+ stdio: ['ignore', 'pipe', 'pipe'],
126
+ });
127
+ if (!retry.error && retry.status === 0 && retry.stdout) {
128
+ const out = retry.stdout.slice(0, OUTPUT_LIMIT).replace(/\r?\n$/, '');
129
+ return out || undefined;
130
+ }
131
+ }
132
+ // Common Keychain errors you may see in retry.stderr:
133
+ // - "User interaction is not allowed." (Keychain locked / non-interactive / no UI permission)
134
+ // - "The specified item could not be found in the keychain."
135
+ return undefined;
136
+ }
137
+ /* ============================ macOS fallback (scutil) ============================ */
138
+ function parseMacScutil() {
139
+ const out = spawnSync('/usr/sbin/scutil', ['--proxy'], {
140
+ encoding: 'utf8',
141
+ windowsHide: true,
142
+ shell: false,
143
+ });
144
+ if (out.error || !out.stdout)
145
+ return null;
146
+ const map = {};
147
+ for (const line of out.stdout.split(/\r?\n/)) {
148
+ const m = line.match(/^\s*([A-Za-z0-9]+)\s*:\s*(.+?)\s*$/);
149
+ if (m)
150
+ map[m[1]] = m[2];
151
+ }
152
+ const info = {};
153
+ // PAC + autodetect
154
+ if (map['ProxyAutoConfigEnable'] === '1' && map['ProxyAutoConfigURLString']) {
155
+ info.pacUrl = map['ProxyAutoConfigURLString'].trim();
156
+ }
157
+ if (map['ProxyAutoDiscoveryEnable'] === '1')
158
+ info.autoDetect = true;
159
+ // Collect per-protocol creds (Apple keys vary by macOS version)
160
+ const httpUser = pick(map, ['HTTPProxyUsername', 'HTTPUser', 'HTTPUsername']);
161
+ let httpPass = pick(map, ['HTTPProxyPassword', 'HTTPPassword']);
162
+ const httpsUser = pick(map, ['HTTPSProxyUsername', 'HTTPSUser', 'HTTPSUsername']);
163
+ let httpsPass = pick(map, ['HTTPSProxyPassword', 'HTTPSPassword']);
164
+ const socksUser = pick(map, ['SOCKSProxyUsername', 'SOCKSUser', 'SOCKSUsername']);
165
+ let socksPass = pick(map, ['SOCKSProxyPassword', 'SOCKSPassword']);
166
+ // Manual proxies (always set host:port only; never include creds)
167
+ if (map['HTTPEnable'] === '1' && map['HTTPProxy'] && map['HTTPPort']) {
168
+ const hostPort = `${map['HTTPProxy']}:${map['HTTPPort']}`;
169
+ info.http = hostPort;
170
+ // If macOS has username but no password, try Keychain for this host/account
171
+ if (httpUser && !httpPass)
172
+ httpPass = keychainFindInternetPassword(extractHost(hostPort), httpUser);
173
+ }
174
+ if (map['HTTPSEnable'] === '1' && map['HTTPSProxy'] && map['HTTPSPort']) {
175
+ const hostPort = `${map['HTTPSProxy']}:${map['HTTPSPort']}`;
176
+ info.https = hostPort;
177
+ if (httpsUser && !httpsPass)
178
+ httpsPass = keychainFindInternetPassword(extractHost(hostPort), httpsUser);
179
+ }
180
+ if (map['SOCKSEnable'] === '1' && map['SOCKSProxy'] && map['SOCKSPort']) {
181
+ const hostPort = `${map['SOCKSProxy']}:${map['SOCKSPort']}`;
182
+ info.socks = hostPort;
183
+ if (socksUser && !socksPass)
184
+ socksPass = keychainFindInternetPassword(extractHost(hostPort), socksUser);
185
+ }
186
+ // Choose one set of creds to expose globally: prefer HTTP, else HTTPS, else SOCKS
187
+ // (Do not overwrite if env already provided username/password.)
188
+ if (!info.username && !info.password) {
189
+ if (httpUser && httpPass) {
190
+ info.username = httpUser;
191
+ info.password = httpPass;
192
+ }
193
+ else if (httpsUser && httpsPass) {
194
+ info.username = httpsUser;
195
+ info.password = httpsPass;
196
+ }
197
+ else if (socksUser && socksPass) {
198
+ info.username = socksUser;
199
+ info.password = socksPass;
200
+ }
201
+ }
202
+ // Bypass list
203
+ let exceptions = [];
204
+ if (map['ExceptionsList']) {
205
+ const quoted = [...map['ExceptionsList'].matchAll(/"([^"]+)"/g)].map(m => m[1]);
206
+ exceptions = quoted.length
207
+ ? quoted
208
+ : map['ExceptionsList'].replace(/[<>{}()]/g, ' ')
209
+ .split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
210
+ }
211
+ if (map['ExcludeSimpleHostnames'] === '1' && !exceptions.includes('<local>'))
212
+ exceptions.unshift('<local>');
213
+ const bypassList = semiJoin(exceptions);
214
+ if (bypassList)
215
+ info.bypassList = bypassList;
216
+ // If scutil did not provide creds anywhere, still allow env creds as global fallback
217
+ if ((!info.username || !info.password)) {
218
+ const envCreds = readCredsFromEnv();
219
+ if (envCreds.username && envCreds.password) {
220
+ info.username = info.username || envCreds.username;
221
+ info.password = info.password || envCreds.password;
222
+ }
223
+ }
224
+ return (info.pacUrl || info.http || info.https || info.socks || info.autoDetect || info.bypassList) ? info : null;
225
+ }
226
+ /* ============================ Windows (Registry only) ============================ */
227
+ function regExeCandidates() {
228
+ const root = process.env.SystemRoot || 'C:\\Windows';
229
+ const sys32 = path.join(root, 'System32', 'reg.exe');
230
+ const syswow64 = path.join(root, 'SysWOW64', 'reg.exe');
231
+ const sysnative = path.join(root, 'Sysnative', 'reg.exe'); // for 32-bit node on 64-bit OS
232
+ const set = new Set([sysnative, sys32, syswow64]);
233
+ return [...set].filter(p => p && fs.existsSync(p));
234
+ }
235
+ function execReg(args) {
236
+ for (const exe of regExeCandidates()) {
237
+ const out = spawnSync(exe, args, {
238
+ encoding: 'utf8',
239
+ windowsHide: true,
240
+ shell: false,
241
+ });
242
+ if (!out.error && out.stdout)
243
+ return out.stdout;
244
+ }
245
+ return null;
246
+ }
247
+ function readRegVals(hiveKey) {
248
+ const stdout = execReg(['query', hiveKey]);
249
+ if (!stdout)
250
+ return null;
251
+ const take = (name) => {
252
+ const m = stdout.match(new RegExp(`\\s${name}\\s+REG_\\w+\\s+(.+)$`, 'mi'));
253
+ return m ? m[1].trim() : '';
254
+ };
255
+ return {
256
+ ProxyEnable: take('ProxyEnable'),
257
+ ProxyServer: take('ProxyServer'),
258
+ ProxyOverride: take('ProxyOverride'),
259
+ AutoConfigURL: take('AutoConfigURL'),
260
+ AutoDetect: take('AutoDetect'),
261
+ };
262
+ }
263
+ function parseWindowsRegistry() {
264
+ const HKCU = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings';
265
+ const HKLM = 'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings';
266
+ const cu = readRegVals(HKCU);
267
+ const lm = readRegVals(HKLM);
268
+ const v = cu || lm;
269
+ if (!v)
270
+ return null;
271
+ const info = {};
272
+ const enabledManual = !!v.ProxyEnable && v.ProxyEnable !== '0' && v.ProxyEnable.toLowerCase() !== '0x0';
273
+ const enabledAuto = !!v.AutoDetect && v.AutoDetect !== '0' && v.AutoDetect.toLowerCase() !== '0x0';
274
+ const anyEnabled = enabledManual || enabledAuto || !!v.AutoConfigURL;
275
+ // PAC + autodetect (only when something is actually enabled)
276
+ if (v.AutoConfigURL)
277
+ info.pacUrl = v.AutoConfigURL.trim(); // PAC stands on its own
278
+ if (enabledAuto)
279
+ info.autoDetect = true; // autodetect still gated
280
+ // Manual proxies
281
+ if (enabledManual && v.ProxyServer) {
282
+ const s = v.ProxyServer.trim();
283
+ if (s.includes('=')) {
284
+ for (const part of s.split(';')) {
285
+ const [proto, addr] = part.split('=');
286
+ if (!proto || !addr)
287
+ continue;
288
+ const p = proto.trim().toLowerCase();
289
+ const a = stripScheme(addr.trim());
290
+ if (p === 'http')
291
+ info.http = a;
292
+ else if (p === 'https')
293
+ info.https = a;
294
+ else if (p === 'socks' || p === 'socks5')
295
+ info.socks = a;
296
+ }
297
+ }
298
+ else {
299
+ const a = stripScheme(s);
300
+ info.http = a;
301
+ info.https = a;
302
+ }
303
+ }
304
+ // Bypass list
305
+ if (anyEnabled && v.ProxyOverride)
306
+ info.bypassList = semiJoin(v.ProxyOverride.split(/[,;]/));
307
+ // Env creds as global fallback (Windows does not store proxy creds in ProxyServer)
308
+ const { username, password } = readCredsFromEnv();
309
+ if (username && password) {
310
+ info.username = username;
311
+ info.password = password;
312
+ }
313
+ return (info.pacUrl || info.http || info.https || info.socks || info.autoDetect || info.bypassList) ? info : null;
314
+ }
315
+ /* ============================ Public API ============================ */
316
+ export function getProxyInfo() {
317
+ const plat = os.platform();
318
+ if (plat === 'win32')
319
+ return parseEnvProxyCommon() || parseWindowsRegistry();
320
+ if (plat === 'darwin')
321
+ return parseEnvProxyCommon() || parseMacScutil();
322
+ return parseEnvProxyCommon(); // Linux/others
323
+ }
324
+ export function proxyInfoToResolution(info) {
325
+ if (!info)
326
+ return { kind: 'none' };
327
+ // Prefer manual proxies first (these work with Playwright's proxy option)
328
+ if (info.http) {
329
+ return { kind: 'manual', settings: {
330
+ server: `http://${info.http}`,
331
+ username: info.username,
332
+ password: info.password,
333
+ bypass: info.bypassList,
334
+ } };
335
+ }
336
+ if (info.https) {
337
+ return { kind: 'manual', settings: {
338
+ server: `http://${info.https}`,
339
+ username: info.username,
340
+ password: info.password,
341
+ bypass: info.bypassList,
342
+ } };
343
+ }
344
+ if (info.socks) {
345
+ return { kind: 'manual', settings: {
346
+ server: `socks5://${info.socks}`,
347
+ username: info.username,
348
+ password: info.password,
349
+ bypass: info.bypassList,
350
+ } };
351
+ }
352
+ // PAC → handle via Chromium args; do NOT try proxy.server = 'pac+...'
353
+ if (info.pacUrl) {
354
+ // Minor hardening: prefer 127.0.0.1 over localhost for loopback PAC
355
+ const pacUrl = info.pacUrl.replace('://localhost', '://127.0.0.1');
356
+ return { kind: 'pac', pacUrl, bypass: info.bypassList };
357
+ }
358
+ // Auto-detect not supported directly
359
+ return { kind: 'none' };
360
+ }
@@ -0,0 +1,16 @@
1
+ import path from 'path';
2
+ import { generateHtmlReport } from './generateHtmlReport.js';
3
+ async function main() {
4
+ const dirArg = process.argv[2];
5
+ if (!dirArg) {
6
+ console.error('Usage: npx tsx src/dev-generate-from-existing.ts <results-dir>');
7
+ process.exit(1);
8
+ }
9
+ const resultDir = path.resolve(process.cwd(), dirArg);
10
+ const out = await generateHtmlReport(resultDir);
11
+ console.log('\nOpen:', out);
12
+ }
13
+ main().catch((e) => {
14
+ console.error(e);
15
+ process.exit(1);
16
+ });