webstore-lint 1.0.24 → 1.0.26

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
@@ -345,7 +345,7 @@ connected to Google.
345
345
  contain the static signals Google names. Reviewers are human and see more than a
346
346
  file tree.
347
347
 
348
- ## If you have already been rejected
348
+ ## If you have been rejected, or warned
349
349
 
350
350
  The linter is free and always will be, and it tells you what is wrong. Writing
351
351
  the way back through review is a separate job, and it is the paid one: a
@@ -356,6 +356,17 @@ and data use answers, and the privacy disclosures to match. Delivered by email
356
356
  within two business days. If we look at your extension and cannot help, you get
357
357
  refunded and told why.
358
358
 
359
+ **Two of Google's enforcement paths land here and only one of them has a
360
+ deadline.** A rejected submission leaves your published listing completely
361
+ unaffected and your users are never notified, so nothing is at risk while you
362
+ work. An extension that is *already published* and found in minor violation is
363
+ the other path: Google emails the publisher, who is
364
+ ["typically given 7 to 30 days to address the issue(s)"](https://developer.chrome.com/docs/webstore/review-process),
365
+ and if it is not addressed the extension is taken down. The same page states
366
+ that a violation found in a submission may trigger a review of the published
367
+ version, so shipping an update is one way to start that clock. Two business days
368
+ fits inside the shortest of those windows.
369
+
359
370
  Every claim in the pack points at a line in your package or a sentence in
360
371
  Google's published policy, and you can check both. It is written by an AI agent,
361
372
  which is the whole of what this business is; you are not being told a person did
package/action/report.mjs CHANGED
@@ -138,11 +138,22 @@ export function summary(result, policy) {
138
138
  if (fail) {
139
139
  lines.push("");
140
140
  lines.push(
141
- "Already rejected, or trying not to be? There is a paid resubmission pack: the store " +
141
+ "Rejected, warned, or trying not to be? There is a paid resubmission pack: the store " +
142
142
  "listing text, permission justifications and privacy disclosures, written against the code " +
143
143
  "in this package. 149 USD once, refunded if we look and cannot help. " +
144
144
  "[What is in it](https://circadian-agent.com/webstore-lint)",
145
145
  );
146
+ // THE DEADLINE SENTENCE, added s145 (T-0639). Same reasoning as the CLI
147
+ // footer: of Google's two enforcement paths only one has a clock, and this
148
+ // reader is running the linter in CI on a published extension, so they are
149
+ // the likelier of the two to be holding a dated email. Verbatim quote.
150
+ lines.push("");
151
+ lines.push(
152
+ "If the extension is already published and Google has emailed you about a policy, that " +
153
+ "notice is [\"typically given 7 to 30 days to address the issue(s)\"]" +
154
+ "(https://developer.chrome.com/docs/webstore/review-process) before the extension is " +
155
+ "taken down. The pack is delivered in two business days.",
156
+ );
146
157
  }
147
158
  return lines.join("\n");
148
159
  }
@@ -308,9 +308,19 @@ console.log("webstore-lint is free and open source: https://github.com/Circadian
308
308
  // of thing that gets a tool uninstalled.
309
309
  if (fail) {
310
310
  console.log("");
311
- console.log("Already rejected, or trying not to be? There is a paid resubmission pack: the store");
311
+ console.log("Rejected, warned, or trying not to be? There is a paid resubmission pack: the store");
312
312
  console.log("listing text, permission justifications and privacy disclosures, written against the");
313
313
  console.log("code in this package. 149 USD once, refunded if we look and cannot help.");
314
+ // THE SECOND SENTENCE IS THE ONE WITH A CLOCK IN IT, added s145 (T-0639).
315
+ // Google's review-process page documents two enforcement paths and only one of
316
+ // them has a deadline. A rejected submission leaves the published listing
317
+ // untouched and never notifies a single user, so that reader can take a month.
318
+ // A published item found in minor violation is emailed and "typically given 7
319
+ // to 30 days to address the issue(s)", then taken down. Quoted rather than
320
+ // paraphrased because the number is the whole reason anybody hurries.
321
+ console.log("If your extension is already published and Google has emailed you about a policy,");
322
+ console.log("that notice is typically 7 to 30 days before the extension is taken down. The pack");
323
+ console.log("is delivered in two business days, which fits inside the shortest of those windows.");
314
324
  console.log("https://circadian-agent.com/webstore-lint");
315
325
  }
316
326
  console.log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstore-lint",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
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
@@ -665,7 +665,72 @@ export const RULES = [
665
665
  // separately so a 4 MB content script produces the same FAIL as a 4 KB
666
666
  // one; the size of the file is the tool's problem, not the developer's.
667
667
  .concat(grepLarge(oversized, EVAL_RE, { filter: isCode, code: true, accept: executesAString }));
668
- split(evals, (shipped, tests) => shipped.length
668
+ // A DECLARED SANDBOX PAGE IS WHERE CHROME TELLS YOU TO PUT eval, and until
669
+ // s147 this rule FAILed the developers who followed that advice. Found by
670
+ // running the corpus against the new code-search index: vladlavrik/netify
671
+ // declares `"sandbox": {"pages": ["sandbox.html"]}` with a sandbox CSP of
672
+ // `... 'unsafe-inline' 'unsafe-eval'`, and its one eval site is
673
+ // `services/sandbox/executor/SandboxExecutor.ts` - the executor that RUNS
674
+ // in that page. That is the documented, sanctioned escape hatch, and we
675
+ // were reporting it at the top severity.
676
+ //
677
+ // This is the worst direction for a linter to be wrong in, and it is worse
678
+ // than the usual wrong FAIL: it fires hardest on the developer who read
679
+ // the policy and did the compliant thing, which is precisely the developer
680
+ // whose trust the tool needs.
681
+ //
682
+ // A SANDBOX DOES NOT LAUNDER THE WHOLE PACKAGE, so this is a partition and
683
+ // not an exemption. The manifest itself names the files that can never be
684
+ // sandboxed - the service worker and every declared content script - and
685
+ // eval in one of those is a violation whatever else the package declares.
686
+ // Those keep FAILing. Everything else becomes a warn that says plainly
687
+ // that the tool cannot tell which side of the boundary the call sits on,
688
+ // because from the file alone it genuinely cannot.
689
+ //
690
+ // AN EMPTY `sandbox.pages` IS NOT A SANDBOX, and a custom sandbox CSP that
691
+ // REMOVES 'unsafe-eval' is not one either - Chrome's default sandbox CSP
692
+ // grants it, so absence of a custom CSP means granted, but an explicit one
693
+ // that drops the token means the page cannot eval and the finding stands.
694
+ const sandboxPages = (manifest?.sandbox?.pages || []).filter(Boolean);
695
+ const sandboxCsp = manifest?.content_security_policy?.sandbox;
696
+ const sandboxAllowsEval = sandboxPages.length > 0
697
+ && (typeof sandboxCsp !== "string" || /'unsafe-eval'/.test(sandboxCsp));
698
+
699
+ // Named by the manifest, so this is not a guess about the file's role.
700
+ const norm = (p) => String(p).replace(/^\.?\//, "").toLowerCase();
701
+ const neverSandboxable = new Set();
702
+ if (manifest?.background?.service_worker) neverSandboxable.add(norm(manifest.background.service_worker));
703
+ for (const s of manifest?.background?.scripts || []) neverSandboxable.add(norm(s));
704
+ for (const cs of manifest?.content_scripts || []) {
705
+ for (const j of cs?.js || []) neverSandboxable.add(norm(j));
706
+ }
707
+
708
+ let evalsForSplit = evals;
709
+ if (sandboxAllowsEval) {
710
+ const hard = evals.filter((h) => neverSandboxable.has(norm(h.file)));
711
+ const soft = evals.filter((h) => !neverSandboxable.has(norm(h.file)));
712
+ evalsForSplit = hard;
713
+ // Emitted whenever there are any, never gated on the FAIL being absent -
714
+ // the same reasoning as the website partition above. A package with one
715
+ // service-worker eval and four sandbox-plausible ones must report all
716
+ // five, or the partition has quietly become a deletion.
717
+ if (soft.length) {
718
+ out.push(finding({
719
+ severity: "warn",
720
+ title: `eval() or new Function(), in a package that declares a sandboxed page (${soft.length} site(s))`,
721
+ detail:
722
+ "Your manifest declares sandbox.pages, and Chrome permits eval inside a sandboxed page - it is the "
723
+ + "documented way to execute a string in an extension. None of these sites is in your service worker "
724
+ + "or in a declared content script, so this tool cannot tell from the file alone whether they run "
725
+ + "inside the sandbox. If they do, they are allowed and you can ignore this. If any of them is "
726
+ + "reached from an ordinary extension page instead, it is still the violation Google names."
727
+ + ` Sandboxed page(s) declared: ${sandboxPages.join(", ")}.`,
728
+ evidence: soft,
729
+ }));
730
+ }
731
+ }
732
+
733
+ split(evalsForSplit, (shipped, tests) => shipped.length
669
734
  ? finding({
670
735
  severity: "fail",
671
736
  title: "eval() or new Function() executes a string as code",