webstore-lint 1.0.30 → 1.0.32

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
@@ -64,6 +64,10 @@ git clone https://github.com/Circadian-agent/chrome-extension-rejection-lint
64
64
  node chrome-extension-rejection-lint/bin/webstore-lint.mjs ./my-extension
65
65
  ```
66
66
 
67
+ Pass one extension directory per command. If you maintain several extensions,
68
+ run the command separately for each one. Multiple directories, including those
69
+ expanded from a shell wildcard, are rejected before any checks run.
70
+
67
71
  ## In GitHub Actions
68
72
 
69
73
  ```yaml
@@ -343,6 +347,10 @@ What it reports:
343
347
  It checks *reachability*. It does not judge whether your policy is adequate,
344
348
  which is the thing a reviewer actually reads it for.
345
349
 
350
+ The request has a ten-second deadline, including downloading the page body.
351
+ A timeout or interrupted download produces a network warning, with no verdict
352
+ on the unread page's content.
353
+
346
354
  ## What it cannot do
347
355
 
348
356
  It reads your package. It cannot see your store listing, your screenshots or your
@@ -70,7 +70,8 @@ for (const f of VALUED) {
70
70
  const i = args.indexOf(f);
71
71
  if (i >= 0 && args[i + 1] && !args[i + 1].startsWith("--")) consumed.add(i + 1);
72
72
  }
73
- const target = args.find((a, i) => !a.startsWith("--") && !consumed.has(i));
73
+ const targets = args.filter((a, i) => !a.startsWith("--") && !consumed.has(i));
74
+ const target = targets[0];
74
75
  const policyUrl = valueOf("--privacy-policy");
75
76
 
76
77
  if (flags.has("--help") || (!target && !flags.has("--policy"))) {
@@ -89,6 +90,7 @@ if (flags.has("--help") || (!target && !flags.has("--policy"))) {
89
90
  webstore-lint --policy list the policy data this build carries
90
91
 
91
92
  Use --permissions, --privacy-policy and --policy in separate commands.
93
+ Check one extension directory per command.
92
94
 
93
95
  The one-liner, no install needed:
94
96
  npx webstore-lint <extension-directory>
@@ -102,6 +104,14 @@ not affiliated with Google. A clean run is not a promise of approval.`);
102
104
  process.exit(flags.has("--help") ? 0 : 2);
103
105
  }
104
106
 
107
+ // A second directory (including a shell-expanded glob) must not be silently
108
+ // ignored: the first extension can pass while the unexamined one would fail.
109
+ if (targets.length > 1) {
110
+ console.error("Pass only one extension directory per command. No checks were run.");
111
+ console.error("Run webstore-lint separately for each extension directory.");
112
+ process.exit(2);
113
+ }
114
+
105
115
  // These branches produce different reports and return early. Combining them
106
116
  // used to exit successfully after silently skipping an explicitly requested
107
117
  // check, including a broken privacy URL. Reject before producing any report.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstore-lint",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
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"
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"
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",
package/src/privacy.mjs CHANGED
@@ -118,6 +118,7 @@ export async function checkPolicyUrl(url, { timeoutMs = 10000, fetchImpl = fetch
118
118
  const ctl = new AbortController();
119
119
  const timer = setTimeout(() => ctl.abort(), timeoutMs);
120
120
  let res;
121
+ let body;
121
122
  try {
122
123
  res = await fetchImpl(u.href, {
123
124
  signal: ctl.signal,
@@ -129,24 +130,30 @@ export async function checkPolicyUrl(url, { timeoutMs = 10000, fetchImpl = fetch
129
130
  "user-agent": "webstore-lint (privacy policy reachability check)",
130
131
  },
131
132
  });
133
+ // fetch resolves at headers. Keep the deadline active until the body is
134
+ // complete too, or a stalled page can hang the CLI and Action indefinitely.
135
+ if (res.ok) body = await res.text();
136
+ else ctl.abort(); // The status is enough evidence; do not download its body.
132
137
  } catch (e) {
133
- clearTimeout(timer);
134
138
  // COULD NOT LOOK IS NOT THE SAME FACT AS NOT THERE. A DNS failure on the
135
139
  // developer's own machine, an offline CI runner or a proxy is not evidence
136
140
  // about the URL, so this warns and says which it is rather than failing.
137
- const aborted = e.name === "AbortError";
141
+ const aborted = ctl.signal.aborted || e.name === "AbortError";
138
142
  return [...findings, f(
139
143
  "warn",
140
144
  aborted
141
- ? `The privacy policy URL did not answer within ${timeoutMs / 1000}s`
142
- : `The privacy policy URL could not be reached from here: ${e.message}`,
145
+ ? `The privacy policy URL did not finish loading within ${timeoutMs / 1000}s`
146
+ : res
147
+ ? `The privacy policy response could not be read from here: ${e.message}`
148
+ : `The privacy policy URL could not be reached from here: ${e.message}`,
143
149
  "This is not the same as the address being broken. It may be your network, a proxy, or an offline " +
144
150
  "runner. Open it yourself in a private browser window before you trust either answer. " +
145
151
  "A reviewer will be doing exactly that.",
146
152
  [{ file: "--privacy-policy", text: url }],
147
153
  )];
154
+ } finally {
155
+ clearTimeout(timer);
148
156
  }
149
- clearTimeout(timer);
150
157
 
151
158
  const finalUrl = res.url || u.href;
152
159
  const redirected = finalUrl.replace(/\/$/, "") !== u.href.replace(/\/$/, "");
@@ -163,13 +170,6 @@ export async function checkPolicyUrl(url, { timeoutMs = 10000, fetchImpl = fetch
163
170
  )];
164
171
  }
165
172
 
166
- let body = "";
167
- try {
168
- body = await res.text();
169
- } catch {
170
- body = "";
171
- }
172
-
173
173
  // Strip tags and scripts before counting words, or a single-page app's bundle
174
174
  // reads as a wall of text that mentions everything.
175
175
  const text = body