webstore-lint 1.0.26 → 1.0.28
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/bin/webstore-lint.mjs +7 -1
- package/package.json +2 -2
- package/src/rules.mjs +40 -2
- package/src/scan.mjs +46 -0
package/bin/webstore-lint.mjs
CHANGED
|
@@ -131,7 +131,10 @@ if (flags.has("--permissions")) {
|
|
|
131
131
|
const { audit: a, root, manifestError } = auditPermissions(target);
|
|
132
132
|
if (manifestError) { console.error(manifestError); process.exit(1); }
|
|
133
133
|
|
|
134
|
-
if (flags.has("--json")) {
|
|
134
|
+
if (flags.has("--json")) {
|
|
135
|
+
await new Promise((resolve) => process.stdout.write(JSON.stringify(a, null, 2) + "\n", resolve));
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
135
138
|
|
|
136
139
|
const cite = POLICY.categories["excessive-permissions"];
|
|
137
140
|
console.log(`\nwebstore-lint permission ledger ${root}`);
|
|
@@ -173,6 +176,9 @@ if (flags.has("--permissions")) {
|
|
|
173
176
|
for (const m of a.confidence.minified) console.log(` minified, so call sites are unreliable: ${m.file} (longest line ${m.longestLine} chars)`);
|
|
174
177
|
for (const s of a.confidence.skipped) console.log(` not read: ${s.path} (${s.why})`);
|
|
175
178
|
console.log("");
|
|
179
|
+
// Drain the text report before exiting too. A slow pipe can otherwise lose
|
|
180
|
+
// the minified/skipped warnings needed to interpret the permission evidence.
|
|
181
|
+
await new Promise((resolve) => process.stdout.write("", resolve));
|
|
176
182
|
process.exit(0);
|
|
177
183
|
}
|
|
178
184
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webstore-lint",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"description": "Check an unpacked Chrome extension against the Chrome Web Store program policies before you submit it. Every finding cites Google's own notification ID and verbatim policy text. Covers the 1 August 2026 policy updates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chrome-extension",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"node": ">=18"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
|
-
"test": "node test/lint.test.mjs && node test/action.test.mjs"
|
|
41
|
+
"test": "node test/lint.test.mjs && node test/action.test.mjs && node --test test/permissions-output.test.mjs"
|
|
42
42
|
},
|
|
43
43
|
"repository": {
|
|
44
44
|
"type": "git",
|
package/src/rules.mjs
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// is worth less than none. When in doubt a rule warns and says what a human
|
|
19
19
|
// must check.
|
|
20
20
|
|
|
21
|
-
import { grep, grepAcross, grepLarge, largeWindows, isCode, isMarkup, codeView, excerptAround } from "./scan.mjs";
|
|
21
|
+
import { grep, grepAcross, grepLarge, largeWindows, isCode, isMarkup, codeView, excerptAround, htmlCommentRanges, insideHtmlComment, offsetOf } from "./scan.mjs";
|
|
22
22
|
import {
|
|
23
23
|
MANIFEST_EVIDENCE, PERMISSION_API, NO_NAMESPACE_PERMISSIONS,
|
|
24
24
|
namespaceUsed, looksMinified, looksMinifiedLarge, bareImports,
|
|
@@ -441,7 +441,45 @@ export const RULES = [
|
|
|
441
441
|
evidence: website,
|
|
442
442
|
}));
|
|
443
443
|
}
|
|
444
|
-
|
|
444
|
+
// COMMENTED-OUT TAGS ARE THEIR OWN ANSWER, NOT A FAILURE AND NOT SILENCE.
|
|
445
|
+
// A <script src> inside <!-- --> does not load and cannot be rejected for,
|
|
446
|
+
// so failing on it is the wrong-fail case that gets a linter muted. We did
|
|
447
|
+
// exactly that to google/archat, whose options/options.html carries a
|
|
448
|
+
// commented-out Google Analytics block - the single most visible repo in
|
|
449
|
+
// our corpus, reported at the highest severity the tool has.
|
|
450
|
+
//
|
|
451
|
+
// IT GETS ITS OWN LABEL RATHER THAN JOINING THE WEBSITE OR TEST WARNS
|
|
452
|
+
// (T-0620). Those two sentences say "this file probably does not ship";
|
|
453
|
+
// this one says "this file ships and the tag is one edit from live". Three
|
|
454
|
+
// different facts, three different words. Overloading one of the existing
|
|
455
|
+
// ones would tell a developer to check their build output for a file that
|
|
456
|
+
// is already in it.
|
|
457
|
+
//
|
|
458
|
+
// Measured before it was written: 14 of 703 remote <script src> sites
|
|
459
|
+
// across 6,351 HTML files in the corpus cache are inside a comment, in six
|
|
460
|
+
// distinct repos.
|
|
461
|
+
const commentCache = new Map();
|
|
462
|
+
const inComment = (h) => {
|
|
463
|
+
const f = files.find((x) => x.path === h.file);
|
|
464
|
+
if (!f) return false; // cannot tell -> resolve toward FAIL, as elsewhere in this rule
|
|
465
|
+
if (!commentCache.has(f.path)) commentCache.set(f.path, htmlCommentRanges(f.text));
|
|
466
|
+
return insideHtmlComment(commentCache.get(f.path), offsetOf(f, h.line, h.col));
|
|
467
|
+
};
|
|
468
|
+
const commentedOut = remoteScript.filter(inComment);
|
|
469
|
+
const remoteScriptLive = remoteScript.filter((h) => !inComment(h));
|
|
470
|
+
if (commentedOut.length) {
|
|
471
|
+
out.push(finding({
|
|
472
|
+
severity: "warn",
|
|
473
|
+
title: `A <script> tag loading remote code is commented out (${commentedOut.length} site(s))`,
|
|
474
|
+
detail:
|
|
475
|
+
"Every site below sits inside an HTML comment, so it does not load and Google cannot reject you for it "
|
|
476
|
+
+ "as it stands. It is reported because it ships in the package and is one edit from live: if you "
|
|
477
|
+
+ "uncomment it before submitting, it becomes the first trigger Google names for this category. Delete "
|
|
478
|
+
+ "it, or vendor the file into the package before you re-enable it.",
|
|
479
|
+
evidence: commentedOut,
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
split(remoteScriptLive, (shipped, tests) => shipped.length
|
|
445
483
|
? finding({
|
|
446
484
|
severity: "fail",
|
|
447
485
|
title: "A <script> tag loads code from outside the extension package",
|
package/src/scan.mjs
CHANGED
|
@@ -396,6 +396,52 @@ export function grepAcross(files, re, filter = () => true) {
|
|
|
396
396
|
export const isCode = (f) => [".js", ".mjs", ".cjs", ".ts", ".jsx", ".tsx"].includes(f.ext);
|
|
397
397
|
export const isMarkup = (f) => [".html", ".htm"].includes(f.ext);
|
|
398
398
|
|
|
399
|
+
// Where a grepAcross hit sits in the file, from the line and column it reports.
|
|
400
|
+
//
|
|
401
|
+
// grepAcross deliberately returns line+col rather than the raw offset, because
|
|
402
|
+
// that is what evidence carries and an extra field on a hit ends up in the
|
|
403
|
+
// published report. A caller that needs the offset back can ask for it here
|
|
404
|
+
// instead of every caller re-deriving it slightly differently.
|
|
405
|
+
export function offsetOf(file, line, col) {
|
|
406
|
+
let at = 0;
|
|
407
|
+
for (let i = 0; i < line - 1; i++) at += (file.lines[i]?.length ?? 0) + 1; // +1 for the newline
|
|
408
|
+
return at + col;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// The byte ranges of every HTML comment in a document.
|
|
412
|
+
//
|
|
413
|
+
// WHY A LINTER NEEDS THIS AT ALL. A commented-out <script src="https://..."> is
|
|
414
|
+
// not remote code. It does not load, it does not run, and Google cannot reject
|
|
415
|
+
// an extension for it. Reporting one at FAIL - the highest severity this tool
|
|
416
|
+
// has - is the wrong-fail case that gets a linter muted, and it happened on the
|
|
417
|
+
// most visible repo in our own corpus: google/archat carries a commented-out
|
|
418
|
+
// Google Analytics block in options/options.html and we called it a policy
|
|
419
|
+
// violation. Measured across 6,351 HTML files in the 1,079-repo clone cache,
|
|
420
|
+
// 14 of 703 remote <script src> sites (2.0%) are inside a comment.
|
|
421
|
+
//
|
|
422
|
+
// AN UNTERMINATED `<!--` IS TREATED AS NOT-A-COMMENT, which is deliberate and is
|
|
423
|
+
// the one place this disagrees with a browser (an unclosed comment really does
|
|
424
|
+
// swallow the rest of the document). The rule that uses this resolves every
|
|
425
|
+
// ambiguity toward FAIL, the same convention the off-package test in rules.mjs
|
|
426
|
+
// already follows, because a missed real violation is recoverable and a
|
|
427
|
+
// confident wrong one is not believed twice. An unterminated comment is also
|
|
428
|
+
// vanishingly rare next to the case this exists for.
|
|
429
|
+
export function htmlCommentRanges(text) {
|
|
430
|
+
const ranges = [];
|
|
431
|
+
let i = 0;
|
|
432
|
+
for (;;) {
|
|
433
|
+
const open = text.indexOf("<!--", i);
|
|
434
|
+
if (open === -1) break;
|
|
435
|
+
const close = text.indexOf("-->", open + 4);
|
|
436
|
+
if (close === -1) break; // unterminated: see above, NOT a comment
|
|
437
|
+
ranges.push([open, close + 3]);
|
|
438
|
+
i = close + 3;
|
|
439
|
+
}
|
|
440
|
+
return ranges;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export const insideHtmlComment = (ranges, at) => ranges.some(([s, e]) => at >= s && at < e);
|
|
444
|
+
|
|
399
445
|
// Blank out comments, keeping every other byte where it was.
|
|
400
446
|
//
|
|
401
447
|
// WHY THIS LIVES HERE rather than in the one rule that first needed it: a
|