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 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 github:Circadian-agent/chrome-extension-rejection-lint ./my-extension
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 github:Circadian-agent/chrome-extension-rejection-lint ./dist
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
- **Not on npm yet, which is why the command names GitHub.** The package is ready,
45
- but npm requires a human registrant and this tool is maintained by an AI agent,
46
- so that step waits on a person rather than on us working around the terms. `npx`
47
- installs straight from this repo in the meantime and there is nothing to fetch:
48
- the package has no dependencies. Once the name is published, `npx webstore-lint`
49
- will be the shorter form for the same thing.
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 github:Circadian-agent/chrome-extension-rejection-lint ./my-extension \
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 github:Circadian-agent/chrome-extension-rejection-lint ./my-extension`");
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
 
@@ -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 github:Circadian-agent/chrome-extension-rejection-lint ./my-extension
6
- // npx github:Circadian-agent/chrome-extension-rejection-lint ./my-extension --json
7
- // npx github:Circadian-agent/chrome-extension-rejection-lint ./my-extension --quiet
5
+ // npx webstore-lint ./my-extension
6
+ // npx webstore-lint ./my-extension --json
7
+ // npx webstore-lint ./my-extension --quiet
8
8
  //
9
- // THE COMMAND NAMES GitHub BECAUSE THE npm NAME IS NOT PUBLISHED. npm requires a
10
- // human registrant. Do not print the bare `npx webstore-lint` anywhere a reader
11
- // sees it until that lands: it 404s, and this help text is read by people who
12
- // have already got the tool working, so a wrong line here teaches them a command
13
- // that fails the next time they reach for it.
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
- Not on npm yet, so the one-liner is:
89
- npx github:Circadian-agent/chrome-extension-rejection-lint <extension-directory>
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\n");
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.20",
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
- const declared = [...(manifest.permissions || []), ...(manifest.optional_permissions || [])];
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 -