webstore-lint 1.0.33 → 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 +14 -1
- package/package.json +2 -2
- package/src/rules.mjs +22 -6
- 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.
|
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
|
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) {
|