webstore-lint 1.0.21 → 1.0.22
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 +18 -0
- package/action/report.mjs +13 -0
- package/bin/webstore-lint.mjs +15 -1
- package/package.json +1 -1
- package/src/rules.mjs +59 -2
package/README.md
CHANGED
|
@@ -341,6 +341,24 @@ connected to Google.
|
|
|
341
341
|
contain the static signals Google names. Reviewers are human and see more than a
|
|
342
342
|
file tree.
|
|
343
343
|
|
|
344
|
+
## If you have already been rejected
|
|
345
|
+
|
|
346
|
+
The linter is free and always will be, and it tells you what is wrong. Writing
|
|
347
|
+
the way back through review is a separate job, and it is the paid one: a
|
|
348
|
+
**resubmission pack, 149 USD once**. It is the text the Web Store dashboard makes
|
|
349
|
+
you produce, written against the code in your own package - the single purpose
|
|
350
|
+
description, a justification for every permission you request, the remote code
|
|
351
|
+
and data use answers, and the privacy disclosures to match. Delivered by email
|
|
352
|
+
within two business days. If we look at your extension and cannot help, you get
|
|
353
|
+
refunded and told why.
|
|
354
|
+
|
|
355
|
+
Every claim in the pack points at a line in your package or a sentence in
|
|
356
|
+
Google's published policy, and you can check both. It is written by an AI agent,
|
|
357
|
+
which is the whole of what this business is; you are not being told a person did
|
|
358
|
+
it.
|
|
359
|
+
|
|
360
|
+
[What is in the pack](https://circadian-agent.com/webstore-lint)
|
|
361
|
+
|
|
344
362
|
## Provenance
|
|
345
363
|
|
|
346
364
|
Policy data is generated from a dataset gathered on 2026-07-29 by direct HTTPS
|
package/action/report.mjs
CHANGED
|
@@ -131,6 +131,19 @@ export function summary(result, policy) {
|
|
|
131
131
|
}
|
|
132
132
|
lines.push("");
|
|
133
133
|
lines.push("Run it yourself: `npx webstore-lint ./my-extension`");
|
|
134
|
+
// FAILING RUNS ONLY, and it keys on `fail` rather than on the exit code. With
|
|
135
|
+
// `fail-on: never` the check stays green while the findings are still there,
|
|
136
|
+
// and the reader still has the problem the pack answers. A run with warnings
|
|
137
|
+
// and no failures gets nothing, and a clean run returned above this line.
|
|
138
|
+
if (fail) {
|
|
139
|
+
lines.push("");
|
|
140
|
+
lines.push(
|
|
141
|
+
"Already rejected, or trying not to be? There is a paid resubmission pack: the store " +
|
|
142
|
+
"listing text, permission justifications and privacy disclosures, written against the code " +
|
|
143
|
+
"in this package. 149 USD once, refunded if we look and cannot help. " +
|
|
144
|
+
"[What is in it](https://circadian-agent.com/webstore-lint)",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
134
147
|
return lines.join("\n");
|
|
135
148
|
}
|
|
136
149
|
|
package/bin/webstore-lint.mjs
CHANGED
|
@@ -299,7 +299,21 @@ console.log(
|
|
|
299
299
|
"This reads your package only. Your store listing, privacy policy page and screenshots are " +
|
|
300
300
|
"where several of these are actually satisfied, and no local tool can see them.",
|
|
301
301
|
);
|
|
302
|
-
console.log("webstore-lint is free and open source: https://github.com/Circadian-agent/chrome-extension-rejection-lint
|
|
302
|
+
console.log("webstore-lint is free and open source: https://github.com/Circadian-agent/chrome-extension-rejection-lint");
|
|
303
|
+
// THE PAID PACK IS MENTIONED ON FAILING RUNS ONLY, and the condition is the
|
|
304
|
+
// whole point. A clean run means the reader does not have this problem, and
|
|
305
|
+
// putting an offer in front of them there is advertising rather than an answer.
|
|
306
|
+
// A failing run means they are holding it right now. Keep it to three lines and
|
|
307
|
+
// keep the price and the refund in them: a footer that hides either is the kind
|
|
308
|
+
// of thing that gets a tool uninstalled.
|
|
309
|
+
if (fail) {
|
|
310
|
+
console.log("");
|
|
311
|
+
console.log("Already rejected, or trying not to be? There is a paid resubmission pack: the store");
|
|
312
|
+
console.log("listing text, permission justifications and privacy disclosures, written against the");
|
|
313
|
+
console.log("code in this package. 149 USD once, refunded if we look and cannot help.");
|
|
314
|
+
console.log("https://circadian-agent.com/webstore-lint");
|
|
315
|
+
}
|
|
316
|
+
console.log("");
|
|
303
317
|
// Same trap, same reason: the human report is also large on a real extension
|
|
304
318
|
// and it is also piped (`webstore-lint . | less`, or into a file in CI). Setting
|
|
305
319
|
// the code instead of calling exit() lets Node drain stdout and then leave with
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webstore-lint",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
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",
|
package/src/rules.mjs
CHANGED
|
@@ -547,16 +547,60 @@ export const RULES = [
|
|
|
547
547
|
// sites are real-looking calls (`eval('ale'+'rt(0)')`) that happen to sit
|
|
548
548
|
// in a string, and nothing local separates them from a genuine call, so
|
|
549
549
|
// they are LEFT FAILING rather than guessed at.
|
|
550
|
+
//
|
|
551
|
+
// SAME PROOF, ONE MORE SHAPE (s135, T-0413's second widening). `new
|
|
552
|
+
// Function("")` builds a function with an EMPTY BODY - calling it does
|
|
553
|
+
// nothing and returns undefined, exactly the no-op EVAL_NO_ARGS already
|
|
554
|
+
// carves out, just spelled with the other string-execution primitive.
|
|
555
|
+
// It is not a rare shape: it is pdf.js's own `isEvalSupported()` -
|
|
556
|
+
// `try { new Function(""); return true; } catch { return false; }` -
|
|
557
|
+
// vendored, byte-for-byte or minified, into every extension that bundles
|
|
558
|
+
// PDF rendering, and it is AngularJS's `noUnsafeEval()` doing the same
|
|
559
|
+
// CSP probe with `new Function('')`. Measured on this widened corpus:
|
|
560
|
+
// magnetgrouplabs/myjdownloader-extension-mv3 (angular.js), listen1's
|
|
561
|
+
// vendored angular.min.js, and pdf.js in both MLabPages/
|
|
562
|
+
// classroom-office-reviewer and ttop32/MouseTooltipTranslator all FAILed
|
|
563
|
+
// on nothing but this literal - four unrelated repos wrongly failed by
|
|
564
|
+
// one vendored idiom whose entire purpose is CHECKING FOR CSP-SAFETY,
|
|
565
|
+
// which makes the false accusation an especially bad look: the tool
|
|
566
|
+
// fails a package for the very check that proves it is eval-aware.
|
|
550
567
|
const BUNDLER_GLOBAL_SHIM = /^new\s+Function\s*\(\s*(["'])return this\1\s*\)/;
|
|
551
568
|
const EVAL_METHOD_DEF = /^eval\s*\(\s*(?:[A-Za-z_$][\w$]*(?:\s*,\s*[A-Za-z_$][\w$]*)*)?\s*\)\s*\{/;
|
|
552
569
|
const EVAL_NO_ARGS = /^eval\s*\(\s*\)/;
|
|
570
|
+
const NEW_FUNCTION_EMPTY_BODY = /^new\s+Function\s*\(\s*(["'`])\s*\1\s*\)/;
|
|
571
|
+
// EVAL_METHOD_DEF ONLY CATCHES *UNTYPED* METHOD DEFINITIONS (s135, T-0413's
|
|
572
|
+
// second widening). Tardo/OdooTerminal declares
|
|
573
|
+
// `async eval(code: string, options?: Partial<EvalOptions>, isolated_frame?:
|
|
574
|
+
// boolean = false): Promise<any> {` on its terminal Shell class - a method
|
|
575
|
+
// named `eval` for evaluating a TERMINAL COMMAND, unrelated to the JS
|
|
576
|
+
// engine's `eval`. It is a method definition by the same logic as
|
|
577
|
+
// `openstyles/stylus`'s untyped one (a call is never followed by `){`... or
|
|
578
|
+
// here, by a TYPE), but EVAL_METHOD_DEF requires a plain identifier list
|
|
579
|
+
// immediately closed by `)\s*{`, and TypeScript/Flow signatures add type
|
|
580
|
+
// annotations, `?` optional markers, generics and default values that push
|
|
581
|
+
// the closing `{` well past this rule's 80-character lookahead window - so
|
|
582
|
+
// neither the old regex nor a wider version of it can reliably see the
|
|
583
|
+
// brace that proves this is a definition.
|
|
584
|
+
//
|
|
585
|
+
// THE SAFE SIGNAL DOES NOT NEED THE BRACE. `identifier:` (optionally
|
|
586
|
+
// `identifier?:`) directly inside the parens of `eval(...)` is not valid
|
|
587
|
+
// JavaScript or TypeScript CALL syntax - a bare `name: value` pair inside
|
|
588
|
+
// plain parens is a SyntaxError unless it is a parameter's type annotation
|
|
589
|
+
// in a function/method signature (an object-literal argument would need its
|
|
590
|
+
// own `{`, and a ternary's `cond ? a : b` always has whitespace or a `?`
|
|
591
|
+
// between the identifier and the colon that this pattern does not allow).
|
|
592
|
+
// So seeing `eval(` immediately followed by a plain identifier and a colon
|
|
593
|
+
// is sufficient on its own, well within the 80-character window, and cannot
|
|
594
|
+
// be produced by any real call to the global `eval`.
|
|
595
|
+
const EVAL_METHOD_DEF_TYPED = /^eval\s*\(\s*[A-Za-z_$][\w$]*\??\s*:/;
|
|
553
596
|
const executesAString = (m, line) => {
|
|
554
597
|
// m[1] is the leading non-identifier char of the eval branch, which is
|
|
555
598
|
// part of the match but not part of the site.
|
|
556
599
|
const at = m.index + (m[1] ? m[1].length : 0);
|
|
557
600
|
const tail = line.slice(at, at + 80);
|
|
558
601
|
return !BUNDLER_GLOBAL_SHIM.test(tail) && !EVAL_METHOD_DEF.test(tail)
|
|
559
|
-
&& !EVAL_NO_ARGS.test(tail)
|
|
602
|
+
&& !EVAL_NO_ARGS.test(tail) && !NEW_FUNCTION_EMPTY_BODY.test(tail)
|
|
603
|
+
&& !EVAL_METHOD_DEF_TYPED.test(tail);
|
|
560
604
|
};
|
|
561
605
|
const source = new Map(files.map((f) => [f.path, f.lines]));
|
|
562
606
|
const EVAL_RE = /(^|[^.\w$])eval\s*\(|new\s+Function\s*\(/;
|
|
@@ -931,7 +975,20 @@ export const RULES = [
|
|
|
931
975
|
category: "excessive-permissions",
|
|
932
976
|
run({ manifest, files, skipped = [], oversized = [] }) {
|
|
933
977
|
if (!manifest) return [];
|
|
934
|
-
|
|
978
|
+
// DEDUPED, because a manifest can and does list the same permission
|
|
979
|
+
// twice. bonigarcia/browserwatcher's `permissions` array is
|
|
980
|
+
// `["tabs","tabCapture","activeTab","storage","offscreen","storage"]` -
|
|
981
|
+
// a genuine copy-paste duplicate in the source, found widening the
|
|
982
|
+
// corpus (T-0413, s135). Without this, the finding read "Declared but
|
|
983
|
+
// never used: storage, storage", which looks like the TOOL is
|
|
984
|
+
// malfunctioning (double-counting) even though the underlying claim -
|
|
985
|
+
// storage really is unused - was correct either time. A `Set` cannot
|
|
986
|
+
// change which permissions are judged unused (the filter below still
|
|
987
|
+
// sees every distinct permission name exactly once, same as before for
|
|
988
|
+
// every manifest that does not have this bug), so this only removes a
|
|
989
|
+
// confusing repeated entry from the reported text; it cannot turn a
|
|
990
|
+
// true finding into a false one or vice versa.
|
|
991
|
+
const declared = [...new Set([...(manifest.permissions || []), ...(manifest.optional_permissions || [])])];
|
|
935
992
|
// Comments blanked first. A permission whose only trace is "// we used to
|
|
936
993
|
// call chrome.bookmarks.getTree here" is the COMMONEST real form of an
|
|
937
994
|
// unused permission - the feature was deleted and the note left behind -
|