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 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
 
@@ -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\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("");
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.21",
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 -