@govtechsg/oobee 0.10.85 → 0.10.87
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/.github/workflows/publish.yml +10 -0
- package/DETAILS.md +29 -0
- package/dist/cli.js +18 -5
- package/dist/combine.js +3 -1
- package/dist/constants/cliFunctions.js +2 -2
- package/dist/constants/common.js +70 -17
- package/dist/constants/constants.js +604 -1
- package/dist/crawlers/commonCrawlerFunc.js +3 -2
- package/dist/crawlers/crawlDomain.js +38 -13
- package/dist/crawlers/crawlIntelligentSitemap.js +62 -30
- package/dist/crawlers/crawlSitemap.js +141 -84
- package/dist/crawlers/custom/utils.js +218 -71
- package/dist/crawlers/guards/urlGuard.js +8 -15
- package/dist/crawlers/runCustom.js +18 -11
- package/dist/generateHtmlReport.js +18 -11
- package/dist/generateOobeeClientScanner.js +570 -0
- package/dist/mergeAxeResults/itemReferences.js +60 -25
- package/dist/mergeAxeResults/sentryTelemetry.js +4 -1
- package/dist/mergeAxeResults.js +23 -13
- package/dist/npmIndex.js +10 -2
- package/dist/proxyService.js +18 -3
- package/dist/services/s3Uploader.js +21 -10
- package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +6 -3
- package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
- package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
- package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +38 -2
- package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +1 -1
- package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +4 -4
- package/dist/static/ejs/summary.ejs +19 -8
- package/dist/utils.js +4 -3
- package/fix-summary-html-oom-pr.md +62 -0
- package/oobee-client-scanner.js +34992 -0
- package/package.json +5 -5
- package/src/cli.ts +19 -5
- package/src/combine.ts +5 -1
- package/src/constants/cliFunctions.ts +2 -2
- package/src/constants/common.ts +87 -22
- package/src/constants/constants.ts +602 -1
- package/src/crawlers/commonCrawlerFunc.ts +4 -3
- package/src/crawlers/crawlDomain.ts +39 -13
- package/src/crawlers/crawlIntelligentSitemap.ts +63 -30
- package/src/crawlers/crawlSitemap.ts +165 -100
- package/src/crawlers/custom/utils.ts +241 -80
- package/src/crawlers/guards/urlGuard.ts +24 -31
- package/src/crawlers/runCustom.ts +29 -11
- package/src/generateHtmlReport.ts +21 -11
- package/src/generateOobeeClientScanner.ts +591 -0
- package/src/mergeAxeResults/itemReferences.ts +70 -26
- package/src/mergeAxeResults/sentryTelemetry.ts +4 -1
- package/src/mergeAxeResults.ts +26 -14
- package/src/npmIndex.ts +12 -2
- package/src/proxyService.ts +25 -4
- package/src/services/s3Uploader.ts +23 -11
- package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +6 -3
- package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
- package/src/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
- package/src/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +38 -2
- package/src/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +1 -1
- package/src/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +4 -4
- package/src/static/ejs/summary.ejs +19 -8
- package/src/utils.ts +4 -3
- package/testStaticJSScanner.html +534 -0
package/dist/mergeAxeResults.js
CHANGED
|
@@ -5,7 +5,7 @@ import printMessage from 'print-message';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import ejs from 'ejs';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
-
import constants, { BrowserTypes, ScannerTypes, WCAGclauses, a11yRuleShortDescriptionMap, disabilityBadgesMap, a11yRuleLongDescriptionMap, } from './constants/constants.js';
|
|
8
|
+
import constants, { BrowserTypes, ScannerTypes, WCAGclauses, a11yRuleShortDescriptionMap, disabilityBadgesMap, a11yRuleLongDescriptionMap, a11yRuleStepByStepGuide, } from './constants/constants.js';
|
|
9
9
|
import { getBrowserToRun, getPlaywrightLaunchOptions } from './constants/common.js';
|
|
10
10
|
import { createScreenshotsFolder, getStoragePath, getVersion, getWcagPassPercentage, getProgressPercentage, retryFunction, zipResults, getIssuesPercentage, register, } from './utils.js';
|
|
11
11
|
import { consoleLogger } from './logs.js';
|
|
@@ -119,10 +119,10 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report', scanDe
|
|
|
119
119
|
const { topFilePath, bottomFilePath } = await splitHtmlAndCreateFiles(htmlFilePath, storagePath);
|
|
120
120
|
const prefixData = fs.readFileSync(path.join(storagePath, 'report-partial-top.htm.txt'), 'utf-8');
|
|
121
121
|
const suffixData = fs.readFileSync(path.join(storagePath, 'report-partial-bottom.htm.txt'), 'utf-8');
|
|
122
|
-
// Create lighter
|
|
123
|
-
const
|
|
122
|
+
// Create the lighter scanItems payload for embedding in the HTML report.
|
|
123
|
+
const lightScanItemsPayload = convertItemsToReferences(allIssues);
|
|
124
124
|
// Write the lighter items to a file and get the base64 path
|
|
125
|
-
const { jsonFilePath:
|
|
125
|
+
const { jsonFilePath: lightScanItemsPayloadJsonFilePath, base64FilePath: lightScanItemsPayloadBase64FilePath, } = await writeJsonFileAndCompressedJsonFile(lightScanItemsPayload, storagePath, 'scanItems-light');
|
|
126
126
|
return new Promise((resolve, reject) => {
|
|
127
127
|
const scanDetailsReadStream = fs.createReadStream(scanDetailsFilePath, {
|
|
128
128
|
encoding: 'utf8',
|
|
@@ -135,8 +135,8 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report', scanDe
|
|
|
135
135
|
await Promise.all([
|
|
136
136
|
fs.promises.unlink(topFilePath),
|
|
137
137
|
fs.promises.unlink(bottomFilePath),
|
|
138
|
-
fs.promises.unlink(
|
|
139
|
-
fs.promises.unlink(
|
|
138
|
+
fs.promises.unlink(lightScanItemsPayloadBase64FilePath),
|
|
139
|
+
fs.promises.unlink(lightScanItemsPayloadJsonFilePath),
|
|
140
140
|
]);
|
|
141
141
|
}
|
|
142
142
|
catch (err) {
|
|
@@ -172,22 +172,28 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report', scanDe
|
|
|
172
172
|
} else {
|
|
173
173
|
console.warn('Skipping fetch GenAI feature as it is local report');
|
|
174
174
|
}
|
|
175
|
+
|
|
176
|
+
var scanData = null;
|
|
177
|
+
var scanItems = null;
|
|
175
178
|
\n`);
|
|
176
179
|
outputStream.write('</script>\n<script type="text/plain" id="scanDataRaw">');
|
|
177
180
|
scanDetailsReadStream.pipe(outputStream, { end: false });
|
|
178
181
|
scanDetailsReadStream.on('end', async () => {
|
|
179
182
|
outputStream.write('</script>\n<script>\n');
|
|
180
|
-
outputStream.write("var scanDataPromise = (async () => { console.log('Loading scanData...'); scanData = await decodeUnzipParse(document.getElementById('scanDataRaw').textContent); })();\n");
|
|
183
|
+
outputStream.write("var scanDataPromise = (async () => { console.log('Loading scanData...'); scanData = await decodeUnzipParse(document.getElementById('scanDataRaw').textContent); console.log('[report] scanData loaded'); })();\n");
|
|
181
184
|
outputStream.write('</script>\n');
|
|
182
185
|
// Write scanItems in 2MB chunks using a stream to avoid loading entire file into memory
|
|
183
186
|
try {
|
|
184
187
|
let chunkIndex = 1;
|
|
185
|
-
const scanItemsStream = fs.createReadStream(
|
|
188
|
+
const scanItemsStream = fs.createReadStream(lightScanItemsPayloadBase64FilePath, {
|
|
186
189
|
encoding: 'utf8',
|
|
187
190
|
highWaterMark: CHUNK_SIZE,
|
|
188
191
|
});
|
|
189
192
|
for await (const chunk of scanItemsStream) {
|
|
190
|
-
outputStream.write(`<script type="text/plain" id="scanItemsRaw${chunkIndex}">${chunk}</script>\n`);
|
|
193
|
+
const ok = outputStream.write(`<script type="text/plain" id="scanItemsRaw${chunkIndex}">${chunk}</script>\n`);
|
|
194
|
+
if (!ok) {
|
|
195
|
+
await new Promise(resolve => outputStream.once('drain', resolve));
|
|
196
|
+
}
|
|
191
197
|
chunkIndex++;
|
|
192
198
|
}
|
|
193
199
|
outputStream.write('<script>\n');
|
|
@@ -203,6 +209,7 @@ var scanItemsPromise = (async () => {
|
|
|
203
209
|
i++;
|
|
204
210
|
}
|
|
205
211
|
scanItems = await decodeUnzipParse(chunks);
|
|
212
|
+
console.log('[report] scanItems loaded');
|
|
206
213
|
})();\n`);
|
|
207
214
|
outputStream.write(suffixData);
|
|
208
215
|
outputStream.end();
|
|
@@ -302,9 +309,9 @@ const wcagOccurrencesMap = new Map();
|
|
|
302
309
|
const pushResults = async (pageResults, allIssues, isCustomFlow) => {
|
|
303
310
|
const { url, pageTitle, filePath } = pageResults;
|
|
304
311
|
const totalIssuesInPage = new Set();
|
|
305
|
-
Object.keys(pageResults.mustFix
|
|
306
|
-
Object.keys(pageResults.goodToFix
|
|
307
|
-
Object.keys(pageResults.needsReview
|
|
312
|
+
Object.keys(pageResults.mustFix?.rules ?? {}).forEach(k => totalIssuesInPage.add(k));
|
|
313
|
+
Object.keys(pageResults.goodToFix?.rules ?? {}).forEach(k => totalIssuesInPage.add(k));
|
|
314
|
+
Object.keys(pageResults.needsReview?.rules ?? {}).forEach(k => totalIssuesInPage.add(k));
|
|
308
315
|
allIssues.topFiveMostIssues.push({
|
|
309
316
|
url,
|
|
310
317
|
pageTitle,
|
|
@@ -592,6 +599,7 @@ generateJsonFiles = false) => {
|
|
|
592
599
|
a11yRuleShortDescriptionMap,
|
|
593
600
|
disabilityBadgesMap,
|
|
594
601
|
a11yRuleLongDescriptionMap,
|
|
602
|
+
a11yRuleStepByStepGuide,
|
|
595
603
|
wcagCriteriaLabels: constants.wcagCriteriaLabels,
|
|
596
604
|
scanPagesDetail: {
|
|
597
605
|
pagesAffected: [],
|
|
@@ -721,11 +729,13 @@ generateJsonFiles = false) => {
|
|
|
721
729
|
const browserChannel = getBrowserToRun(randomToken, BrowserTypes.CHROME, false).browserToRun;
|
|
722
730
|
// Should consider refactor constants.userDataDirectory to be a parameter in future
|
|
723
731
|
await retryFunction(() => writeSummaryPdf(storagePath, pagesScanned.length, 'summary', browserChannel, constants.userDataDirectory), 1);
|
|
732
|
+
// Brief delay to allow lingering async crawlee storage operations to flush
|
|
733
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
724
734
|
try {
|
|
725
735
|
await fs.promises.rm(path.join(storagePath, 'crawlee'), { recursive: true, force: true });
|
|
726
736
|
}
|
|
727
737
|
catch (error) {
|
|
728
|
-
|
|
738
|
+
// Silently ignore — folder may already be gone or still locked
|
|
729
739
|
}
|
|
730
740
|
try {
|
|
731
741
|
await fs.promises.rm(path.join(storagePath, 'pdfs'), { recursive: true, force: true });
|
package/dist/npmIndex.js
CHANGED
|
@@ -4,7 +4,7 @@ import axe from 'axe-core';
|
|
|
4
4
|
import { JSDOM } from 'jsdom';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { EnqueueStrategy } from 'crawlee';
|
|
7
|
-
import constants, { BrowserTypes, RuleFlags, ScannerTypes } from './constants/constants.js';
|
|
7
|
+
import constants, { BrowserTypes, RuleFlags, ScannerTypes, a11yRuleShortDescriptionMap, a11yRuleLongDescriptionMap, a11yRuleStepByStepGuide } from './constants/constants.js';
|
|
8
8
|
import { deleteClonedProfiles, getBrowserToRun, getPlaywrightLaunchOptions, submitForm, } from './constants/common.js';
|
|
9
9
|
import { createCrawleeSubFolders, filterAxeResults } from './crawlers/commonCrawlerFunc.js';
|
|
10
10
|
import { createAndUpdateResultsFolders, getVersion } from './utils.js';
|
|
@@ -489,6 +489,10 @@ const processAndSubmitResults = async (scanData, name, email, metadata) => {
|
|
|
489
489
|
if (constants.a11yRuleShortDescriptionMap[ruleId]) {
|
|
490
490
|
mergedResults[category].rules[ruleId].description = constants.a11yRuleShortDescriptionMap[ruleId];
|
|
491
491
|
}
|
|
492
|
+
// Add short description, long description and step-by-step guide
|
|
493
|
+
mergedResults[category].rules[ruleId].shortDescription = a11yRuleShortDescriptionMap[ruleId];
|
|
494
|
+
mergedResults[category].rules[ruleId].longDescription = a11yRuleLongDescriptionMap[ruleId];
|
|
495
|
+
mergedResults[category].rules[ruleId].stepByStepGuide = a11yRuleStepByStepGuide[ruleId];
|
|
492
496
|
// Add url to items
|
|
493
497
|
mergedResults[category].rules[ruleId].items.forEach((item) => {
|
|
494
498
|
item.url = result.url;
|
|
@@ -554,6 +558,10 @@ const processAndSubmitResults = async (scanData, name, email, metadata) => {
|
|
|
554
558
|
if (constants.a11yRuleShortDescriptionMap[rule.rule]) {
|
|
555
559
|
rule.description = constants.a11yRuleShortDescriptionMap[rule.rule];
|
|
556
560
|
}
|
|
561
|
+
// Add short description, long description and step-by-step guide
|
|
562
|
+
rule.shortDescription = a11yRuleShortDescriptionMap[rule.rule];
|
|
563
|
+
rule.longDescription = a11yRuleLongDescriptionMap[rule.rule];
|
|
564
|
+
rule.stepByStepGuide = a11yRuleStepByStepGuide[rule.rule];
|
|
557
565
|
if (rule.items) {
|
|
558
566
|
rule.items.forEach((item) => {
|
|
559
567
|
// Ensure item URL matches the result URL
|
|
@@ -637,4 +645,4 @@ export const scanPage = async (pages, config) => {
|
|
|
637
645
|
}
|
|
638
646
|
return processAndSubmitResults(scanData, name, email, metadata);
|
|
639
647
|
};
|
|
640
|
-
export { RuleFlags };
|
|
648
|
+
export { RuleFlags, a11yRuleLongDescriptionMap, a11yRuleStepByStepGuide, getOobeeFunctionsScript };
|
package/dist/proxyService.js
CHANGED
|
@@ -57,7 +57,7 @@ function parseEnvProxyCommon() {
|
|
|
57
57
|
if (https)
|
|
58
58
|
info.https = stripScheme(https);
|
|
59
59
|
if (socks)
|
|
60
|
-
info.socks =
|
|
60
|
+
info.socks = socks; // keep original scheme so proxyInfoToResolution can use the right protocol
|
|
61
61
|
if (noProxy)
|
|
62
62
|
info.bypassList = semiJoin(noProxy.split(/[,;]/));
|
|
63
63
|
const { username, password } = readCredsFromEnv();
|
|
@@ -384,6 +384,14 @@ function buildIncludeOnlyPac(proxyServer, includeList) {
|
|
|
384
384
|
].join('\n');
|
|
385
385
|
return pac;
|
|
386
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Convert an info.socks value to a full proxy server URL.
|
|
389
|
+
* When the value already carries a scheme (e.g. ALL_PROXY=http://..., socks4://...),
|
|
390
|
+
* it is used as-is. Bare host:port values (from scutil) default to socks5://.
|
|
391
|
+
*/
|
|
392
|
+
function toSocksServer(socks) {
|
|
393
|
+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(socks) ? socks : `socks5://${socks}`;
|
|
394
|
+
}
|
|
387
395
|
export function proxyInfoToResolution(info) {
|
|
388
396
|
if (!info)
|
|
389
397
|
return { kind: 'none' };
|
|
@@ -396,7 +404,7 @@ export function proxyInfoToResolution(info) {
|
|
|
396
404
|
else if (info.https)
|
|
397
405
|
proxyServer = `http://${info.https}`;
|
|
398
406
|
else if (info.socks)
|
|
399
|
-
proxyServer =
|
|
407
|
+
proxyServer = toSocksServer(info.socks);
|
|
400
408
|
if (proxyServer) {
|
|
401
409
|
// If credentials exist, embed them for the manual proxy auth
|
|
402
410
|
// PAC scripts themselves don't carry auth, but Playwright's proxy option can
|
|
@@ -408,6 +416,13 @@ export function proxyInfoToResolution(info) {
|
|
|
408
416
|
const pacDataUrl = `data:application/x-ns-proxy-autoconfig;base64,${Buffer.from(pac).toString('base64')}`;
|
|
409
417
|
return { kind: 'pac', pacUrl: pacDataUrl, bypass: info.bypassList };
|
|
410
418
|
}
|
|
419
|
+
// No direct proxy server was found — the configured proxy is PAC-based or auto-detect only.
|
|
420
|
+
// INCLUDE_PROXY needs a concrete server address to build a routing PAC script, so it cannot
|
|
421
|
+
// be applied here. Warn and fall through to use the existing PAC/autodetect as-is.
|
|
422
|
+
console.warn('INCLUDE_PROXY is set but no direct proxy server address was found. ' +
|
|
423
|
+
'INCLUDE_PROXY requires HTTP_PROXY, HTTPS_PROXY, or ALL_PROXY to be set with a direct ' +
|
|
424
|
+
'server address; it cannot be applied to a PAC URL or auto-detect proxy. ' +
|
|
425
|
+
'INCLUDE_PROXY will be ignored.');
|
|
411
426
|
}
|
|
412
427
|
// Prefer manual proxies first (these work with Playwright's proxy option)
|
|
413
428
|
if (info.http) {
|
|
@@ -428,7 +443,7 @@ export function proxyInfoToResolution(info) {
|
|
|
428
443
|
}
|
|
429
444
|
if (info.socks) {
|
|
430
445
|
return { kind: 'manual', settings: {
|
|
431
|
-
server:
|
|
446
|
+
server: toSocksServer(info.socks),
|
|
432
447
|
username: info.username,
|
|
433
448
|
password: info.password,
|
|
434
449
|
bypass: info.bypassList,
|
|
@@ -5,6 +5,17 @@ import mime from 'mime-types';
|
|
|
5
5
|
import { consoleLogger } from '../logs.js';
|
|
6
6
|
const REGION = process.env.AWS_REGION || 'ap-southeast-1';
|
|
7
7
|
const s3Client = new S3Client({ region: REGION });
|
|
8
|
+
// S3 user metadata is sent over REST as x-amz-meta-* HTTP headers.
|
|
9
|
+
// To avoid request-header validation failures in the Node/AWS SDK path,
|
|
10
|
+
// normalize to printable ASCII before attaching metadata values.
|
|
11
|
+
const sanitizeS3MetadataValue = (value) => {
|
|
12
|
+
return value
|
|
13
|
+
.normalize('NFKD') // e.g. "é" -> "e" + combining accent, "A" -> "A"
|
|
14
|
+
.replace(/[\u0300-\u036f]/g, '') // e.g. remove the combining accent from the decomposed "é"
|
|
15
|
+
.replace(/[^\x20-\x7E]+/g, ' ') // e.g. "公益金" or emoji -> " "
|
|
16
|
+
.replace(/\s+/g, ' ') // e.g. "Community Chest \n" -> "Community Chest "
|
|
17
|
+
.trim(); // e.g. " Homepage | Community Chest " -> "Homepage | Community Chest"
|
|
18
|
+
};
|
|
8
19
|
export const uploadFileToS3 = async (localFilePath, s3Key, metadata) => {
|
|
9
20
|
const fileStream = fs.readFileSync(localFilePath);
|
|
10
21
|
const contentType = mime.lookup(localFilePath) || 'application/octet-stream';
|
|
@@ -38,31 +49,31 @@ export const uploadFolderToS3 = async (localFolderPath, s3Prefix, scanMetadata)
|
|
|
38
49
|
const files = getAllFiles(localFolderPath, localFolderPath);
|
|
39
50
|
const allowedFileExtRegex = /\.(html|csv|pdf|zip)$/;
|
|
40
51
|
const metadata = {
|
|
41
|
-
scanid: scanMetadata.scanId,
|
|
42
|
-
userid: scanMetadata.userId,
|
|
43
|
-
useremail: scanMetadata.email,
|
|
52
|
+
scanid: sanitizeS3MetadataValue(scanMetadata.scanId),
|
|
53
|
+
userid: sanitizeS3MetadataValue(scanMetadata.userId),
|
|
54
|
+
useremail: sanitizeS3MetadataValue(scanMetadata.email),
|
|
44
55
|
};
|
|
45
56
|
// Add optional metadata fields if present
|
|
46
57
|
if (scanMetadata.messageId) {
|
|
47
|
-
metadata.messageid = scanMetadata.messageId;
|
|
58
|
+
metadata.messageid = sanitizeS3MetadataValue(scanMetadata.messageId);
|
|
48
59
|
}
|
|
49
60
|
if (scanMetadata.amplitudeUserId) {
|
|
50
|
-
metadata.amplitudeuserid = scanMetadata.amplitudeUserId;
|
|
61
|
+
metadata.amplitudeuserid = sanitizeS3MetadataValue(scanMetadata.amplitudeUserId);
|
|
51
62
|
}
|
|
52
63
|
if (scanMetadata.deviceId) {
|
|
53
|
-
metadata.deviceid = scanMetadata.deviceId;
|
|
64
|
+
metadata.deviceid = sanitizeS3MetadataValue(scanMetadata.deviceId);
|
|
54
65
|
}
|
|
55
66
|
if (scanMetadata.orgId) {
|
|
56
|
-
metadata.orgid = scanMetadata.orgId;
|
|
67
|
+
metadata.orgid = sanitizeS3MetadataValue(scanMetadata.orgId);
|
|
57
68
|
}
|
|
58
69
|
if (scanMetadata.userRole) {
|
|
59
|
-
metadata.userrole = scanMetadata.userRole;
|
|
70
|
+
metadata.userrole = sanitizeS3MetadataValue(scanMetadata.userRole);
|
|
60
71
|
}
|
|
61
72
|
if (scanMetadata.siteName) {
|
|
62
|
-
metadata.sitename = scanMetadata.siteName;
|
|
73
|
+
metadata.sitename = sanitizeS3MetadataValue(scanMetadata.siteName);
|
|
63
74
|
}
|
|
64
75
|
if (scanMetadata.durationExceeded !== undefined) {
|
|
65
|
-
metadata.durationexceeded = scanMetadata.durationExceeded;
|
|
76
|
+
metadata.durationexceeded = sanitizeS3MetadataValue(scanMetadata.durationExceeded);
|
|
66
77
|
}
|
|
67
78
|
consoleLogger.info(`Uploading ${files.length} files to S3...`);
|
|
68
79
|
const uploadPromises = files.map(async (relativePath) => {
|
|
@@ -28,8 +28,11 @@ async function decodeUnzipParse(input) {
|
|
|
28
28
|
offset += arr.length;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// Step 2: Decompress with pako (GZIP)
|
|
32
|
-
|
|
31
|
+
// Step 2: Decompress with pako (GZIP) to bytes first to avoid large-string
|
|
32
|
+
// construction inside pako for very large payloads.
|
|
33
|
+
const decompressedBytes = pako.ungzip(merged);
|
|
34
|
+
|
|
35
|
+
const decompressed = new TextDecoder().decode(decompressedBytes);
|
|
33
36
|
|
|
34
37
|
// Step 3: Parse JSON
|
|
35
38
|
return JSON.parse(decompressed);
|
|
@@ -37,4 +40,4 @@ async function decodeUnzipParse(input) {
|
|
|
37
40
|
throw new Error(`Failed to decode/unzip/parse: ${err.message}`);
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
|
-
</script>
|
|
43
|
+
</script>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
>
|
|
22
22
|
</div>
|
|
23
23
|
<div class="display-url-container">
|
|
24
|
-
<a href="${page.url}" target="_blank">${page.pageTitle
|
|
24
|
+
<a href="${page.url}" target="_blank">${page.pageTitle?.length > 0 ? page.pageTitle : page.url}</a>
|
|
25
25
|
<p>${page.url}</p>
|
|
26
26
|
</div>
|
|
27
27
|
</div>
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
} else {
|
|
30
30
|
listItem.innerHTML = `
|
|
31
31
|
<a href="${page.url}" target="_blank">
|
|
32
|
-
${page.pageTitle
|
|
32
|
+
${page.pageTitle?.length > 0 ? page.pageTitle : page.url}
|
|
33
33
|
<svg class="link-external-icon" width="16" height="12" viewBox="0 0 8 8" aria-hidden="true" focusable="false">
|
|
34
34
|
<path d="M7.11111 7.11111H0.888889V0.888889H4V0H0.888889C0.395556 0 0 0.4 0 0.888889V7.11111C0 7.6 0.395556 8 0.888889 8H7.11111C7.6 8 8 7.6 8 7.11111V4H7.11111V7.11111ZM4.88889 0V0.888889H6.48444L2.11556 5.25778L2.74222 5.88444L7.11111 1.51556V3.11111H8V0H4.88889Z" fill="#5735DF"/>
|
|
35
35
|
</svg>
|