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 +28 -5
- package/bin/webstore-lint.mjs +1 -0
- package/data/policy.json +23 -29
- package/package.json +2 -2
- package/src/rules.mjs +26 -7
- package/src/scan.mjs +18 -2
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`
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
package/bin/webstore-lint.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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": "
|
|
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
|
|
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": "
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
"
|
|
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 (
|
|
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 ?
|
|
252
|
+
const wasSkipped = wanted ? skippedPath(wanted, skipped) : false;
|
|
237
253
|
|
|
238
254
|
let messages = null;
|
|
239
255
|
if (file) {
|