webstore-lint 1.0.32 → 1.0.34

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
@@ -68,14 +68,27 @@ Pass one extension directory per command. If you maintain several extensions,
68
68
  run the command separately for each one. Multiple directories, including those
69
69
  expanded from a shell wildcard, are rejected before any checks run.
70
70
 
71
+ Symbolic links inside the selected directory are listed as unread and produce a
72
+ scan coverage warning. The scanner does not follow linked files or directories.
73
+ Run it on a copy of the built extension containing regular files to check that
74
+ code. A linked `manifest.json` makes the package unreadable. The Action fails on
75
+ that case at every threshold; other skipped links trigger `fail-on: warn`.
76
+
71
77
  ## In GitHub Actions
72
78
 
73
79
  ```yaml
74
- - uses: Circadian-agent/chrome-extension-rejection-lint@v1
80
+ - uses: Circadian-agent/chrome-extension-rejection-lint@v1.0.33
75
81
  with:
76
82
  path: ./extension
77
83
  ```
78
84
 
85
+ This example pins [v1.0.33](https://github.com/Circadian-agent/chrome-extension-rejection-lint/releases/tag/v1.0.33),
86
+ which includes the privacy-page timeout fix and refreshed Google policy citations.
87
+ The floating `v1` tag still points to v1.0.30 as of 10 October 2026. If your workflow
88
+ uses `@v1`, change it to `@v1.0.33` to include these fixes. Check the
89
+ [release notes](https://github.com/Circadian-agent/chrome-extension-rejection-lint/releases)
90
+ before updating the pin in future.
91
+
79
92
  Findings land on the offending line of the pull request diff, and the verdict
80
93
  goes on the checks tab, so a policy violation is visible where the review already
81
94
  is rather than in a build log nobody opens.
@@ -183,10 +196,13 @@ the tool prints when relevant:
183
196
  1. **The disclosure rule got much wider.** It used to bite only when the data was
184
197
  not closely related to your single purpose. The live policy now covers any
185
198
  user data, disclosed before installation. Meanwhile Google's own
186
- troubleshooting page for `Purple Nickel` still states the old, narrower rule
187
- and still tells developers that undisclosed collection is fine when it matches
188
- the single purpose. That guidance is contradicted by the policy it is meant to
189
- explain.
199
+ troubleshooting page for `Purple Nickel` now also covers any user data, but
200
+ still says before collection rather than before installation. It also still
201
+ conditionally allows collection not disclosed in the listing. The
202
+ [User Data FAQ](https://developer.chrome.com/docs/webstore/program-policies/user-data-faq)
203
+ separately requires disclosure and consent within the product before collection;
204
+ a listing disclosure alone does not satisfy that requirement. Check both the
205
+ listing and the consent flow. These differences were rechecked on 9 October 2026.
190
206
  2. **The prediction markets ban has a carve-out the announcement omits entirely.**
191
207
  Simulated markets with no real money winnings may be allowed, if you clearly
192
208
  say no real money is involved.
@@ -405,6 +421,13 @@ fetch of Google's pages, parsed from raw HTML, with 278 verbatim fields
405
421
  re-checked programmatically against the raw bytes. Nothing in `data/policy.json`
406
422
  is admitted unless it is marked `VERIFIED_GOOGLE`.
407
423
 
424
+ The citations published in `data/policy.json` were rechecked against 23 official
425
+ Google pages on 2026-10-09. This refresh includes the affiliate policy's direct
426
+ user benefit requirement and the installation button disclosure requirement. It
427
+ also removes outdated claims that Google's troubleshooting page still carries
428
+ the old personal-or-sensitive-data scope. Historical research fields in the
429
+ source dataset retain their original measurement dates.
430
+
408
431
  That method was not paranoia. The first pass over the troubleshooting page, done
409
432
  through a summarising model, produced a fabricated policy quote and an invented
410
433
  source structure. A second independent sweep invented a codename that does not
@@ -281,6 +281,7 @@ const wrap = (s, indent = " ", width = 92) => {
281
281
 
282
282
  console.log(`\nwebstore-lint ${result.root}`);
283
283
  console.log(`policy data pulled ${POLICY.datasetPulledAt}, ${POLICY.enforcedFrom} updates included\n`);
284
+ if (POLICY.citationsVerifiedAt) console.log(`policy citations checked ${POLICY.citationsVerifiedAt}\n`);
284
285
 
285
286
  if (!result.findings.length) {
286
287
  console.log("No findings.\n");
package/data/policy.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "schema": 1,
3
3
  "generatedFrom": "research/data/chrome-rejection-reasons.json",
4
4
  "datasetPulledAt": "2026-07-29T00:08:05Z",
5
+ "citationsVerifiedAt": "2026-10-09",
5
6
  "enforcedFrom": "2026-08-01",
6
7
  "enforcementQuote": "Enforcement for these updated policies will begin on August 1, 2026. Extensions found out of compliance after this date may face enforcement action from the Chrome Web Store.",
7
8
  "announcementUrl": "https://developer.chrome.com/blog/cws-policy-updates-2026",
@@ -152,11 +153,7 @@
152
153
  "Visit the Privacy tab for your extension to verify that a privacy policy link appears in the \"Privacy Policy\" box (located at the bottom of the Privacy tab) and that the link works as expected.",
153
154
  "Ensure the privacy policy talks about data collection, usage, handling, and sharing.",
154
155
  "If you have done the above but are still encountering review issues, use the appeal button on the item detail page to appeal the verdict or ask for clarification."
155
- ],
156
- "supersededStillOnTroubleshooting": [
157
- "If your Product handles personal or sensitive user data (including personally identifiable information, financial and payment information, health information, authentication information, website content and resources, form data, web browsing activity, user-provided content and personal communications), then your Product must: Post a privacy policy, and Handle the user data securely, including transmitting it via modern cryptography."
158
- ],
159
- "supersededMeasuredAt": "2026-07-31T13:50:08.088Z"
156
+ ]
160
157
  },
161
158
  "illegal-activities": {
162
159
  "title": "Illegal activities",
@@ -198,7 +195,7 @@
198
195
  "supersededStillOnTroubleshooting": [
199
196
  "We don't allow content or services that facilitate online gambling or prediction markets, including but not limited to online casinos, sports betting, lotteries, or games of skill that offer prizes of cash or other value."
200
197
  ],
201
- "supersededMeasuredAt": "2026-07-31T13:50:08.088Z"
198
+ "supersededMeasuredAt": "2026-10-09"
202
199
  },
203
200
  "pornography": {
204
201
  "title": "Pornographic content",
@@ -225,7 +222,7 @@
225
222
  "If we become aware of content with child pornography, we will report it to the appropriate authorities and delete the Google Accounts of those involved with the distribution.",
226
223
  "Content which contains non-sexual nudity - such as artistic, educational, scientific, or cultural nudity - is generally allowed, but may impact the visibility of your Product."
227
224
  ],
228
- "supersededMeasuredAt": "2026-07-31T13:50:08.088Z"
225
+ "supersededMeasuredAt": "2026-10-09"
229
226
  },
230
227
  "hate": {
231
228
  "title": "Hate content",
@@ -248,7 +245,7 @@
248
245
  "We don't allow content advocating against or inciting hatred towards groups of people based on their race or ethnic origin, religion, disability, gender, age, veteran status, nationality, sexual orientation, gender, gender identity, or any other characteristic that is associated with systematic discrimination or marginalization.",
249
246
  "Additionally, the visibility of your Product may be impacted if it contains generally hateful content not covered by the above definition."
250
247
  ],
251
- "supersededMeasuredAt": "2026-07-31T13:50:08.088Z"
248
+ "supersededMeasuredAt": "2026-10-09"
252
249
  },
253
250
  "not-family-safe": {
254
251
  "title": "Not family safe",
@@ -269,7 +266,7 @@
269
266
  "Our content policies apply to your Product's content, including any ads it shows to users and any user-generated content it hosts or links to.",
270
267
  "Further, they apply to any content from your developer account that is publicly displayed in Chrome Web Store, including your developer name and the landing page of your listed developer website."
271
268
  ],
272
- "supersededMeasuredAt": "2026-07-31T13:50:08.088Z"
269
+ "supersededMeasuredAt": "2026-10-09"
273
270
  },
274
271
  "violence": {
275
272
  "title": "Violent content",
@@ -297,7 +294,7 @@
297
294
  "Content that results in the unwanted sexualization of a person, including malicious claims about a person's sexual activities, sexual orientation, or gender identity.",
298
295
  "A series of posts/comments/photos that, taken together, clearly have the primary intention of harassment, even if each individual piece of content is not severe."
299
296
  ],
300
- "supersededMeasuredAt": "2026-07-31T13:50:08.088Z"
297
+ "supersededMeasuredAt": "2026-10-09"
301
298
  },
302
299
  "single-use": {
303
300
  "title": "Single purpose",
@@ -314,14 +311,14 @@
314
311
  "triggers": [
315
312
  "The extension provides two or more purposes in the same extension (for example an extension providing image format conversion and bibliography generation).",
316
313
  "The extension provides one or more unrelated additional functionalities using the extension's action icon.",
317
- "The extension provides a modified search experience on a new tab page which does not respect the user's choice of search provider or fails to utilize official Chrome search APIs (e.g., chrome.search)."
314
+ "The extension provides a modified search experience on a new tab page which does not respect the user’s choice of search provider or fails to utilize official Chrome search APIs (e.g., chrome.search)."
318
315
  ],
319
316
  "fixes": [
320
317
  "Narrow the functionality of your extension to clearly fit within one well-defined purpose and ensure that purpose is clearly described in your extension's metadata.",
321
318
  "If your extension is offering some functionality and also injecting ads, then either stop injecting ads or remove all other functionality besides ad injection.",
322
319
  "Chrome does not support optional new tab pages. If you wish to provide some functionality and an optional new tab page, you should separate the new tab page into a standalone extension.",
323
320
  "Don't offer another unrelated functionality in the extension's action icon.",
324
- "If your new tab page extension includes a search experience, use the Chrome Search API to ensure it respects the user's choice of search provider."
321
+ "If your new tab page extension includes a search experience, use the Chrome Search API to ensure it respects the user’s choice of search provider."
325
322
  ]
326
323
  },
327
324
  "udp-prominent-disclosure": {
@@ -346,9 +343,10 @@
346
343
  "Consider providing users with an \"offline mode\" that only stores user data locally."
347
344
  ],
348
345
  "supersededStillOnTroubleshooting": [
349
- "If your Product handles personal or sensitive user data that is not closely related to functionality described prominently in the Product's Chrome Web Store page and user interface, then prior to the collection, it must: Prominently disclose how the user data will be used, and Obtain the user's affirmative consent for such use."
346
+ "Prominent Disclosure Requirement: If your Product handles any user data, then prior to the collection, it must: Prominently disclose how the user data will be used. Obtain the user's affirmative consent for such use.",
347
+ "Collecting data that is not prominently disclosed in the Chrome Web Store listing is allowed so long as this data collection is consistent with the extension's single purpose, the user is informed of the data collection before it begins, and the user consents to the data collection."
350
348
  ],
351
- "supersededMeasuredAt": "2026-07-31T13:50:08.088Z"
349
+ "supersededMeasuredAt": "2026-10-09"
352
350
  },
353
351
  "udp-secure": {
354
352
  "title": "User data policy - secure transmission",
@@ -368,11 +366,7 @@
368
366
  "Don't transmit user data over HTTP. If possible, use secure protocols for all requests.",
369
367
  "Don't encode data in request headers or query parameters, even over HTTPS. Headers and request URLs often appear in server logs, which can unintentionally leak this information.",
370
368
  "Use the Chrome DevTools or other network monitoring tools to watch the network requests the extension makes."
371
- ],
372
- "supersededStillOnTroubleshooting": [
373
- "If your Product handles personal or sensitive user data (including personally identifiable information, financial and payment information, health information, authentication information, website content and resources, form data, web browsing activity, user-provided content and personal communications), then your Product must: Post a privacy policy, and Handle the user data securely, including transmitting it via modern cryptography."
374
- ],
375
- "supersededMeasuredAt": "2026-07-31T13:50:08.088Z"
369
+ ]
376
370
  },
377
371
  "udp-other-requirements": {
378
372
  "title": "User data policy - other requirements",
@@ -385,11 +379,11 @@
385
379
  "troubleshootingUrl": "https://developer.chrome.com/docs/webstore/troubleshooting#udp-other-requirements",
386
380
  "triggers": [
387
381
  "The extension is collecting Web Browsing Activity when it is not needed for a user facing feature.",
388
- "Sensitive user information collected using the extension is being disclosed publicly."
382
+ "User data collected using the extension is being disclosed publicly."
389
383
  ],
390
384
  "fixes": [
391
385
  "Don't collect Web Browsing Activity unless it is required for a user-facing feature.",
392
- "Ensure user information is not being disclosed publicly"
386
+ "Ensure user data is not being disclosed publicly."
393
387
  ]
394
388
  },
395
389
  "cryptocurrency-mining": {
@@ -439,7 +433,7 @@
439
433
  "Yellow Argon"
440
434
  ],
441
435
  "plainMeaning": "Your description is padded with keywords, site lists or locations to game search ranking.",
442
- "policyQuote": "Keyword Spam is the practice of including irrelevant or excessive keywords in an extensions description in an attempt to manipulate its ranking, resulting in a spammy, negative user experience. We don't allow extensions with irrelevant or excessive metadata. Developers should focus on providing a clear and well-written description that uses keywords appropriately and in context. Some examples of Keyword Spam include: Lists of sites/brands/keywords without substantial added value Lists of regional locations Unnatural repetition of the same keyword more than 5 times We don't allow unattributed or anonymous user testimonials in the product's description.",
436
+ "policyQuote": "Keyword Spam is the practice of including irrelevant or excessive keywords in an extensions description in an attempt to manipulate its ranking, resulting in a spammy, negative user experience. We don't allow extensions with irrelevant or excessive metadata. Developers should focus on providing a clear and well-written description that uses keywords appropriately and in context. Some examples of Keyword Spam include: Lists of sites/brands/keywords without substantial added value Lists of regional locations Unnatural repetition of the same keyword more than 5 times We don’t allow unattributed or anonymous user testimonials in the product's description.",
443
437
  "policyUrl": "https://developer.chrome.com/docs/webstore/program-policies/listing-requirements",
444
438
  "troubleshootingUrl": "https://developer.chrome.com/docs/webstore/troubleshooting#keyword-stuffing",
445
439
  "triggers": [
@@ -452,7 +446,7 @@
452
446
  "We don't allow extensions with misleading, improperly formatted, non-descriptive, irrelevant, excessive, or inappropriate metadata, including but not limited to the extension's description, developer name, title, icon, screenshots, and promotional images.",
453
447
  "Unattributed or anonymous user testimonials in the product's description."
454
448
  ],
455
- "supersededMeasuredAt": "2026-07-31T13:50:08.088Z"
449
+ "supersededMeasuredAt": "2026-10-09"
456
450
  },
457
451
  "redirection": {
458
452
  "title": "Redirection",
@@ -518,7 +512,7 @@
518
512
  "Red Zinc"
519
513
  ],
520
514
  "plainMeaning": "How you got users to install the extension was misleading, regardless of what the extension itself does.",
521
- "policyQuote": "Extensions must be marketed responsibly. The set of functionalities promised by the extension must be stated clearly and in a transparent manner. The outcome of any user interaction should match the reasonable expectations that were set with the user. Extensions that use or benefit from deceptive installation tactics will be removed from the Chrome Web Store. Deceptive installation tactics include: Confusing or deceptive advertisements or marketing materials preceding the installation of your extension. The features of your extension should be clear when marketing your product. Misleading interactive elements as part of your distribution flow. This includes misleading call-to-action buttons or forms that imply an outcome other than the installation of an extension. Adjusting the Chrome Web Store product listing window with the effect of withholding or hiding extension metadata from the user. Bundling other extensions or offers within the same installation flow. Requiring unrelated user action to access advertised functionality.",
515
+ "policyQuote": "Extensions must be marketed responsibly. The set of functionalities promised by the extension must be stated clearly and in a transparent manner. The outcome of any user interaction should match the reasonable expectations that were set with the user. Extensions that use or benefit from deceptive installation tactics will be removed from the Chrome Web Store. Deceptive installation tactics include: Confusing or deceptive advertisements or marketing materials preceding the installation of your extension. The features of your extension should be clear when marketing your product. Misleading interactive elements as part of your distribution flow. This includes misleading call-to-action buttons or forms that imply an outcome other than the installation of an extension. Any call-to-action buttons preceding the installation of your extension must state that an extension will be installed. Adjusting the Chrome Web Store product listing window with the effect of withholding or hiding extension metadata from the user. Bundling other extensions or offers within the same installation flow. Requiring unrelated user action to access advertised functionality.",
522
516
  "policyUrl": "https://developer.chrome.com/docs/webstore/program-policies/deceptive-installation-tactics",
523
517
  "troubleshootingUrl": "https://developer.chrome.com/docs/webstore/troubleshooting#uws-distribution",
524
518
  "triggers": [
@@ -552,7 +546,7 @@
552
546
  "Yellow Potassium"
553
547
  ],
554
548
  "plainMeaning": "The extension is too thin to be worth listing, or it just links out to a service that does the actual work.",
555
- "policyQuote": "Extensions must provide a basic degree of functionality and utility that provide value to the catalog of the Chrome Web Store. Some examples of common violations include: Extensions with no functionality or utility. Extensions with functionality that is not directly provided by the extension (e.g. file converters which only link to other file conversion services). Click-baity template extensions that only vary slightly in functionality with negligible utility (e.g. a \"Word of the Day\" extension and a \"Daily Inspirational Quotes\" extension, which use the same general extension template).",
549
+ "policyQuote": "Extensions must provide a basic degree of functionality and utility that provide value to the catalog of the Chrome Web Store. Some examples of common violations include: Extensions with no functionality or utility. Extensions with functionality that is not directly provided by the extension (e.g. file converters which only link to other file conversion services). Click-baity template extensions that only vary slightly in functionality with negligible utility (e.g. a “Word of the Day” extension and a “Daily Inspirational Quotes” extension, which use the same general extension template).",
556
550
  "policyUrl": "https://developer.chrome.com/docs/webstore/program-policies/minimum-functionality",
557
551
  "troubleshootingUrl": "https://developer.chrome.com/docs/webstore/troubleshooting#minimum_functionality",
558
552
  "triggers": [
@@ -571,8 +565,8 @@
571
565
  "notificationIds": [
572
566
  "Grey Titanium"
573
567
  ],
574
- "plainMeaning": "You inject affiliate links, codes or cookies without disclosing it and without the user doing something that would lead them to expect it.",
575
- "policyQuote": "Any affiliate program must be described prominently in the product's Chrome Web Store page, user interface, and before installation. Related user action is required before the inclusion of each affiliate code, link, or cookie. Some example violations include: An extension that updates a shopping-related cookie without the user's knowledge while the user is browsing shopping sites. An extension that appends an affiliate code to the URL or replaces an existing affiliate code in the URL without the user's explicit knowledge or related user action. An extension that applies or replaces affiliate promo codes without the user's explicit knowledge or related user action.",
568
+ "plainMeaning": "Affiliate links, codes or cookies need prominent disclosure, a direct user benefit related to the core functionality, and related user action before each inclusion.",
569
+ "policyQuote": "Any affiliate program must be described prominently in the product's Chrome Web Store page, user interface, and before installation. Affiliate links, codes, or cookies must only be included when the extension provides a direct and transparent user benefit related to the extension's core functionality. It is not permitted to inject affiliate links without related user action and without providing a tangible benefit to users. Some common violations include: Inserting affiliate links when no discount, cashback, or donation is provided. An extension that continuously injects affiliate links in the background without related user action. Related user action is required before the inclusion of each affiliate code, link, or cookie. Some example violations include: An extension that updates a shopping-related cookie without the user's knowledge while the user is browsing shopping sites. An extension that appends an affiliate code to the URL or replaces an existing affiliate code in the URL without the user's explicit knowledge or related user action. An extension that applies or replaces affiliate promo codes without the user's explicit knowledge or related user action.",
576
570
  "policyUrl": "https://developer.chrome.com/docs/webstore/program-policies/affiliate-ads",
577
571
  "troubleshootingUrl": "https://developer.chrome.com/docs/webstore/troubleshooting#affiliate_ads",
578
572
  "triggers": [
@@ -598,7 +592,7 @@
598
592
  "Leveraging techniques in an attempt to manipulate the distribution of updates to users and keep them on versions with previous violations"
599
593
  ],
600
594
  "fixes": [
601
- "Don't attempt any further actions to circumvent intended limitations or enforcement actions.",
595
+ "Don’t attempt any further actions to circumvent intended limitations or enforcement actions.",
602
596
  "Continued attempts at enforcement circumvention will result in suspension of your developer account."
603
597
  ]
604
598
  }
@@ -633,7 +627,7 @@
633
627
  "blogQuote": "Disclosure Requirements Policy: We are updating our transparency standards to require that all data collection be prominently disclosed to the user—regardless of whether the data is closely related to the extension's single purpose. Additionally, developers must proactively disclose to users if their data handling practices change at any point after the initial installation.",
634
628
  "announcementUrl": "https://developer.chrome.com/blog/cws-policy-updates-2026",
635
629
  "inLivePolicyText": true,
636
- "discrepancy": "The live Disclosure Requirements page now says 'any user data' and 'prior to installation'. The troubleshooting page for Purple Nickel still quotes the OLD narrower rule ('personal or sensitive user data that is not closely related to functionality described prominently...') and still tells developers that 'Collecting data that is not prominently disclosed in the Chrome Web Store listing is allowed so long as this data collection is consistent with the extension's single purpose'. That guidance is contradicted by the updated policy and is the single most dangerous stale instruction in Google's own documentation right now. MEASURED 2026-07-31: this is not one page. The phrase 'personal or sensitive' appears on NONE of the 18 program policy documents we read, including the single-page version that renders the whole policy at once, so it is absent from the policy rather than merely absent from the page we happened to open. All three pages that state a scope now say 'any user data': Disclosure Requirements, Protecting User Privacy ('If your Product handles any user data, then you must post an accurate and up to date privacy policy') and Data Handling ('If your product collects any user data, it must handle the user data securely, including transmitting it via modern cryptography'). Secure transmission also MOVED off the privacy page onto Data Handling. So a developer who concluded their extension is out of scope because its data is not sensitive is now wrong about the privacy policy requirement and the encryption requirement as well as about disclosure.",
630
+ "discrepancy": "Rechecked 2026-10-09: the troubleshooting quote now covers any user data, so the old scope mismatch is gone. It still says prior to collection, while the live policy says prior to installation. The troubleshooting fixes still allow collection not disclosed in the listing under stated conditions. The User Data FAQ separately says disclosure and consent must occur within the product UI, and that listing disclosures alone do not satisfy that requirement.",
637
631
  "carveOut": null,
638
632
  "notificationId": "Purple Nickel"
639
633
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstore-lint",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
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",
@@ -38,7 +38,7 @@
38
38
  "node": ">=18"
39
39
  },
40
40
  "scripts": {
41
- "test": "node test/lint.test.mjs && node test/action.test.mjs && node --test test/permissions-output.test.mjs test/cli-modes.test.mjs test/privacy-timeout.test.mjs"
41
+ "test": "node test/lint.test.mjs && node test/action.test.mjs && node --test test/permissions-output.test.mjs test/cli-modes.test.mjs test/privacy-timeout.test.mjs test/symlinks.test.mjs"
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",
package/src/rules.mjs CHANGED
@@ -18,7 +18,7 @@
18
18
  // is worth less than none. When in doubt a rule warns and says what a human
19
19
  // must check.
20
20
 
21
- import { grep, grepAcross, grepLarge, largeWindows, isCode, isMarkup, codeView, excerptAround, htmlCommentRanges, insideHtmlComment, offsetOf } from "./scan.mjs";
21
+ import { grep, grepAcross, grepLarge, largeWindows, isCode, isMarkup, codeView, excerptAround, htmlCommentRanges, insideHtmlComment, offsetOf, skippedPath } from "./scan.mjs";
22
22
  import {
23
23
  MANIFEST_EVIDENCE, PERMISSION_API, NO_NAMESPACE_PERMISSIONS,
24
24
  namespaceUsed, looksMinified, looksMinifiedLarge, bareImports,
@@ -157,8 +157,8 @@ function declaredFiles(manifest) {
157
157
  const missingCache = new WeakMap();
158
158
  function missingDeclaredOf(manifest, files, skipped = []) {
159
159
  if (missingCache.has(files)) return missingCache.get(files);
160
- const present = new Set([...files.map((f) => norm(f.path)), ...skipped.map((s) => norm(s.path))]);
161
- const v = declaredFiles(manifest).filter((d) => !present.has(d.path));
160
+ const present = new Set(files.map((f) => norm(f.path)));
161
+ const v = declaredFiles(manifest).filter((d) => !present.has(d.path) && !skippedPath(d.path, skipped));
162
162
  missingCache.set(files, v);
163
163
  return v;
164
164
  }
@@ -167,7 +167,23 @@ function missingDeclaredOf(manifest, files, skipped = []) {
167
167
 
168
168
  export const RULES = [
169
169
 
170
- // FIRST, BECAUSE IT CHANGES HOW YOU READ EVERY OTHER FINDING. If this fires,
170
+ {
171
+ id: "symbolic-links",
172
+ run({ skipped = [] }) {
173
+ const links = skipped.filter(s => s.kind === "symlink");
174
+ if (!links.length) return [];
175
+ return [finding({
176
+ severity: "warn",
177
+ title: `${links.length} symbolic link(s) were not read; scan coverage is incomplete`,
178
+ detail: "Linked files and directories are not followed. Their targets may be outside this package or form a cycle. "
179
+ + "Scan a copy of the built extension containing regular files, then run the checks again. "
180
+ + "This warning describes a limit of this scan, not a Chrome Web Store policy violation.",
181
+ evidence: links.map(s => ({ file: s.path, text: s.why })),
182
+ })];
183
+ },
184
+ },
185
+
186
+ // This changes how you read every policy finding. If this fires,
171
187
  // the directory is not the thing Chrome would review, and every rule that
172
188
  // reasons from the ABSENCE of something is unreliable against it.
173
189
  //
@@ -1094,7 +1110,7 @@ export const RULES = [
1094
1110
  // someone to go looking by hand for a site the tool already found.
1095
1111
  const streamed = new Set(oversized.filter(isCode).map((o) => o.path));
1096
1112
  const unreadCode = skipped.filter((s) =>
1097
- /\.(js|mjs|cjs|ts|jsx|tsx|html?)$/i.test(s.path || "") && !streamed.has(s.path));
1113
+ s.kind !== "symlink" && /\.(js|mjs|cjs|ts|jsx|tsx|html?)$/i.test(s.path || "") && !streamed.has(s.path));
1098
1114
  if (unreadCode.length) {
1099
1115
  out.push(finding({
1100
1116
  severity: "warn",
@@ -1194,7 +1210,7 @@ export const RULES = [
1194
1210
  // read.
1195
1211
  const streamed = new Set(largeCode.map((o) => o.path));
1196
1212
  const unread = skipped.filter((s) =>
1197
- /\.(js|mjs|cjs|ts|jsx|tsx|html?)$/i.test(s.path || "") && !streamed.has(s.path));
1213
+ s.kind === "symlink" || (/\.(js|mjs|cjs|ts|jsx|tsx|html?)$/i.test(s.path || "") && !streamed.has(s.path)));
1198
1214
  // AND THE ONES WE DID READ STILL COUNT TOWARD (2), which is the half that
1199
1215
  // matters. Having read the bytes of a bundle is not having seen the name:
1200
1216
  // over the 82 cached release packages, dropping the unread caveat without
@@ -1744,7 +1760,10 @@ export const RULES = [
1744
1760
  detail:
1745
1761
  "This changed on 1 August 2026 and the change is easy to miss. The old rule only bit when the data was " +
1746
1762
  "NOT closely related to the extension's single purpose. The live policy now covers ANY user data. " +
1747
- "The disclosure lives in your store listing, which this tool cannot see, so verify it yourself. " +
1763
+ "Verify the store listing and consent flow against the live policy. Google's User Data FAQ also " +
1764
+ "requires prominent disclosure and consent within the product before collection; a listing or " +
1765
+ "privacy policy alone does not satisfy that requirement. See " +
1766
+ "https://developer.chrome.com/docs/webstore/program-policies/user-data-faq. " +
1748
1767
  "READ THE WARNING BELOW ABOUT GOOGLE'S OWN TROUBLESHOOTING PAGE.",
1749
1768
  evidence: [
1750
1769
  ...dataPerms.map((p) => ({ file: "manifest.json", line: 1, text: `"${p}"` })),
package/src/scan.mjs CHANGED
@@ -103,6 +103,16 @@ const TEXT_EXT = new Set([
103
103
  ".css", ".txt", ".md", ".webmanifest",
104
104
  ]);
105
105
 
106
+ // A link may name a file or a whole directory. Its descendants are unknown,
107
+ // not proven absent. Do not resolve the target just to make that distinction.
108
+ export function skippedPath(path, skipped) {
109
+ const normalized = path.split(/[\\/]/).join("/");
110
+ return skipped.some(s => {
111
+ const entry = s.path.split(/[\\/]/).join("/");
112
+ return normalized === entry || (s.kind === "symlink" && normalized.startsWith(entry + "/"));
113
+ });
114
+ }
115
+
106
116
  export function scan(root) {
107
117
  const files = [];
108
118
  const skipped = [];
@@ -123,6 +133,10 @@ export function scan(root) {
123
133
  for (const e of entries) {
124
134
  const full = join(dir, e.name);
125
135
  const rel = relative(root, full);
136
+ if (e.isSymbolicLink()) {
137
+ skipped.push({ path: rel, kind: "symlink", why: "symbolic link; target was not read" });
138
+ continue;
139
+ }
126
140
  if (e.isDirectory()) {
127
141
  if (SKIP_DIRS.has(e.name)) { skipped.push({ path: rel, why: "not part of a submitted package" }); continue; }
128
142
  walk(full);
@@ -153,7 +167,9 @@ export function scan(root) {
153
167
  let manifest = null;
154
168
  let manifestError = null;
155
169
  let manifestHint = null;
156
- if (!existsSync(manifestPath)) {
170
+ if (skipped.some(s => s.path === "manifest.json" && s.kind === "symlink")) {
171
+ manifestError = "manifest.json is a symbolic link; its target was not read. Scan a package containing regular files.";
172
+ } else if (!existsSync(manifestPath)) {
157
173
  // NAME THE ACTUAL PROBLEM. "no manifest.json in this directory" was printed
158
174
  // for a path that is not a directory and for a path that does not exist at
159
175
  // all, which sends someone to look for a file in a folder that was never
@@ -233,7 +249,7 @@ export function resolveI18n(manifest, files, skipped = []) {
233
249
  // A locale file the scanner refused to read (over the size limit, unreadable)
234
250
  // is NOT evidence that a message is missing. Staying quiet there is the same
235
251
  // fail-safe direction as everything else in this file.
236
- const wasSkipped = wanted ? skipped.some((s) => s.path.split(/[\\/]/).join("/") === wanted) : false;
252
+ const wasSkipped = wanted ? skippedPath(wanted, skipped) : false;
237
253
 
238
254
  let messages = null;
239
255
  if (file) {