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 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.33",
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
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) {