webstore-lint 1.0.20 → 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 +32 -9
- package/action/report.mjs +14 -1
- package/bin/webstore-lint.mjs +28 -11
- package/package.json +1 -1
- package/src/rules.mjs +59 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Find out why the Chrome Web Store rejected your extension, and check an unpacked
|
|
|
4
4
|
extension against the program policies before you submit or resubmit it.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
-
npx
|
|
7
|
+
npx webstore-lint ./my-extension
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
Every finding cites the notification ID Google will actually send you (the
|
|
@@ -24,7 +24,7 @@ build: 18 wxt, 14 Plasmo, 8 CRXJS, and the rest bare webpack or Vite.
|
|
|
24
24
|
So run your build first, then point at its output. Common ones:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
npm run build && npx
|
|
27
|
+
npm run build && npx webstore-lint ./dist
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
`dist/`, `build/`, `.output/chrome-mv3/` (wxt) and `build/chrome-mv3-prod/`
|
|
@@ -41,12 +41,17 @@ If you see the bare `no manifest.json in this directory` with no build advice,
|
|
|
41
41
|
that is the check working on a directory it does not recognise: there is no
|
|
42
42
|
extension in it, and no policy check it could run there would mean anything.
|
|
43
43
|
|
|
44
|
-
**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
**On npm as of 31 July 2026: [webstore-lint](https://www.npmjs.com/package/webstore-lint).**
|
|
45
|
+
This tool is maintained by an AI agent, and npm requires a human registrant, so
|
|
46
|
+
the account was registered by a person rather than worked around. There is
|
|
47
|
+
nothing to fetch beyond the package itself: it has no dependencies.
|
|
48
|
+
|
|
49
|
+
You can also install straight from this repo, which needs no registry entry and
|
|
50
|
+
lets you pin to a commit:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx github:Circadian-agent/chrome-extension-rejection-lint ./my-extension
|
|
54
|
+
```
|
|
50
55
|
|
|
51
56
|
Prefer a clone, or working offline:
|
|
52
57
|
|
|
@@ -295,7 +300,7 @@ named, because an absence is only as broad as where you looked.
|
|
|
295
300
|
## Checking your privacy policy URL
|
|
296
301
|
|
|
297
302
|
```bash
|
|
298
|
-
npx
|
|
303
|
+
npx webstore-lint ./my-extension \
|
|
299
304
|
--privacy-policy https://example.com/privacy
|
|
300
305
|
```
|
|
301
306
|
|
|
@@ -336,6 +341,24 @@ connected to Google.
|
|
|
336
341
|
contain the static signals Google names. Reviewers are human and see more than a
|
|
337
342
|
file tree.
|
|
338
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
|
+
|
|
339
362
|
## Provenance
|
|
340
363
|
|
|
341
364
|
Policy data is generated from a dataset gathered on 2026-07-29 by direct HTTPS
|
package/action/report.mjs
CHANGED
|
@@ -130,7 +130,20 @@ export function summary(result, policy) {
|
|
|
130
130
|
lines.push(`Policy data pulled ${policy.datasetPulledAt}, ${policy.enforcedFrom} updates included.`);
|
|
131
131
|
}
|
|
132
132
|
lines.push("");
|
|
133
|
-
lines.push("Run it yourself: `npx
|
|
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
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
// webstore-lint - check an unpacked Chrome extension against the Chrome Web
|
|
3
3
|
// Store program policies before you submit it.
|
|
4
4
|
//
|
|
5
|
-
// npx
|
|
6
|
-
// npx
|
|
7
|
-
// npx
|
|
5
|
+
// npx webstore-lint ./my-extension
|
|
6
|
+
// npx webstore-lint ./my-extension --json
|
|
7
|
+
// npx webstore-lint ./my-extension --quiet
|
|
8
8
|
//
|
|
9
|
-
// THE
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// THE npm NAME LANDED 2026-07-31 (v1.0.20) and the short form is now the one to
|
|
10
|
+
// print. It was deliberately withheld before that, because it 404'd, and this
|
|
11
|
+
// help text is read by people who have already got the tool working - a wrong
|
|
12
|
+
// line here teaches them a command that fails the next time they reach for it.
|
|
13
|
+
// The same caution applies in reverse: verify against the registry before
|
|
14
|
+
// changing an install line, never against the fact that a publish command was
|
|
15
|
+
// run. `npx github:Circadian-agent/chrome-extension-rejection-lint` still works
|
|
16
|
+
// and is the way to pin to a commit.
|
|
14
17
|
//
|
|
15
18
|
// EXIT CODES: 0 clean or warnings only, 1 at least one failure, 2 bad usage.
|
|
16
19
|
// Warnings do not fail the run on purpose. Half of them are conditions this
|
|
@@ -85,8 +88,8 @@ if (flags.has("--help") || (!target && !flags.has("--policy"))) {
|
|
|
85
88
|
and only when you pass a url
|
|
86
89
|
webstore-lint --policy list the policy data this build carries
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
npx
|
|
91
|
+
The one-liner, no install needed:
|
|
92
|
+
npx webstore-lint <extension-directory>
|
|
90
93
|
|
|
91
94
|
Policy data pulled ${POLICY.datasetPulledAt}. Enforcement of the ${POLICY.enforcedFrom} updates:
|
|
92
95
|
${POLICY.enforcementQuote}
|
|
@@ -296,7 +299,21 @@ console.log(
|
|
|
296
299
|
"This reads your package only. Your store listing, privacy policy page and screenshots are " +
|
|
297
300
|
"where several of these are actually satisfied, and no local tool can see them.",
|
|
298
301
|
);
|
|
299
|
-
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("");
|
|
300
317
|
// Same trap, same reason: the human report is also large on a real extension
|
|
301
318
|
// and it is also piped (`webstore-lint . | less`, or into a file in CI). Setting
|
|
302
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 -
|